mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 14:30:57 +00:00
fix(runtime): lazy-load setup shims and align contracts
This commit is contained in:
@@ -3,12 +3,12 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig, OpenClawConfig } from "openclaw/plugin-sdk/discord-core";
|
||||
import {
|
||||
mergeDiscordAccountConfig,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordAccountConfig,
|
||||
} from "./accounts.js";
|
||||
import type { DiscordAccountConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
export type DiscordCredentialStatus = "available" | "configured_unavailable" | "missing";
|
||||
|
||||
|
||||
@@ -3,12 +3,8 @@ import {
|
||||
createAccountListHelpers,
|
||||
} from "openclaw/plugin-sdk/account-helpers";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type {
|
||||
DiscordAccountConfig,
|
||||
DiscordActionConfig,
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/discord-core";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import type { DiscordAccountConfig, DiscordActionConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
export type ResolvedDiscordAccount = {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { discordSetupWizard as discordSetupWizardImpl } from "./setup-surface.js";
|
||||
import { createDiscordSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
type DiscordSetupWizard = typeof import("./setup-surface.js").discordSetupWizard;
|
||||
|
||||
export const discordSetupWizard: DiscordSetupWizard = { ...discordSetupWizardImpl };
|
||||
export const discordSetupWizard: DiscordSetupWizard = createDiscordSetupWizardProxy(
|
||||
async () => (await import("./setup-surface.js")).discordSetupWizard,
|
||||
);
|
||||
|
||||
163
extensions/discord/src/setup-account-state.ts
Normal file
163
extensions/discord/src/setup-account-state.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig } from "./runtime-api.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
export type InspectedDiscordSetupAccount = {
|
||||
accountId: string;
|
||||
enabled: boolean;
|
||||
token: string;
|
||||
tokenSource: "env" | "config" | "none";
|
||||
tokenStatus: "available" | "configured_unavailable" | "missing";
|
||||
configured: boolean;
|
||||
config: DiscordAccountConfig;
|
||||
};
|
||||
|
||||
function resolveDiscordAccountEntry(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): DiscordAccountConfig | undefined {
|
||||
const accounts = cfg.channels?.discord?.accounts;
|
||||
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
const direct = accounts[normalized];
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
||||
return matchKey ? accounts[matchKey] : undefined;
|
||||
}
|
||||
|
||||
function inspectConfiguredToken(value: unknown): {
|
||||
token: string;
|
||||
tokenSource: "config";
|
||||
tokenStatus: "available" | "configured_unavailable";
|
||||
} | null {
|
||||
const normalized = normalizeSecretInputString(value);
|
||||
if (normalized) {
|
||||
return {
|
||||
token: normalized.replace(/^Bot\s+/i, ""),
|
||||
tokenSource: "config",
|
||||
tokenStatus: "available",
|
||||
};
|
||||
}
|
||||
if (hasConfiguredSecretInput(value)) {
|
||||
return {
|
||||
token: "",
|
||||
tokenSource: "config",
|
||||
tokenStatus: "configured_unavailable",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function listDiscordSetupAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const accounts = cfg.channels?.discord?.accounts;
|
||||
const ids =
|
||||
accounts && typeof accounts === "object" && !Array.isArray(accounts)
|
||||
? Object.keys(accounts)
|
||||
.map((accountId) => normalizeAccountId(accountId))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
return [...new Set([DEFAULT_ACCOUNT_ID, ...ids])];
|
||||
}
|
||||
|
||||
export function resolveDefaultDiscordSetupAccountId(cfg: OpenClawConfig): string {
|
||||
return listDiscordSetupAccountIds(cfg)[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
export function resolveDiscordSetupAccountConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): { accountId: string; config: DiscordAccountConfig } {
|
||||
const accountId = normalizeAccountId(params.accountId ?? DEFAULT_ACCOUNT_ID);
|
||||
const { accounts: _ignored, ...base } = (params.cfg.channels?.discord ??
|
||||
{}) as DiscordAccountConfig & {
|
||||
accounts?: unknown;
|
||||
};
|
||||
return {
|
||||
accountId,
|
||||
config: {
|
||||
...base,
|
||||
...(resolveDiscordAccountEntry(params.cfg, accountId) ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function inspectDiscordSetupAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): InspectedDiscordSetupAccount {
|
||||
const { accountId, config } = resolveDiscordSetupAccountConfig(params);
|
||||
const enabled = params.cfg.channels?.discord?.enabled !== false && config.enabled !== false;
|
||||
const accountConfig = resolveDiscordAccountEntry(params.cfg, accountId);
|
||||
const hasAccountToken = Boolean(
|
||||
accountConfig &&
|
||||
Object.prototype.hasOwnProperty.call(accountConfig as Record<string, unknown>, "token"),
|
||||
);
|
||||
const accountToken = inspectConfiguredToken(accountConfig?.token);
|
||||
if (accountToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: accountToken.token,
|
||||
tokenSource: accountToken.tokenSource,
|
||||
tokenStatus: accountToken.tokenStatus,
|
||||
configured: true,
|
||||
config,
|
||||
};
|
||||
}
|
||||
if (hasAccountToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
tokenStatus: "missing",
|
||||
configured: false,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
const channelToken = inspectConfiguredToken(params.cfg.channels?.discord?.token);
|
||||
if (channelToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: channelToken.token,
|
||||
tokenSource: channelToken.tokenSource,
|
||||
tokenStatus: channelToken.tokenStatus,
|
||||
configured: true,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
const tokenResolution = resolveDiscordToken(params.cfg, { accountId });
|
||||
if (tokenResolution.token) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: tokenResolution.token,
|
||||
tokenSource: tokenResolution.source,
|
||||
tokenStatus: "available",
|
||||
configured: true,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
tokenStatus: "missing",
|
||||
configured: false,
|
||||
config,
|
||||
};
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { DiscordGuildEntry } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { createEnvPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup-adapter-runtime";
|
||||
import type {
|
||||
ChannelSetupAdapter,
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import {
|
||||
inspectDiscordSetupAccount,
|
||||
listDiscordSetupAccountIds,
|
||||
resolveDiscordSetupAccountConfig,
|
||||
} from "./setup-account-state.js";
|
||||
import {
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createAllowlistSetupWizardProxy,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
createEnvPatchedAccountSetupAdapter,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
setSetupChannelEnabled,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
createAllowlistSetupWizardProxy,
|
||||
type ChannelSetupAdapter,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import { listDiscordAccountIds, resolveDiscordAccount } from "./accounts.js";
|
||||
} from "./setup-runtime-helpers.js";
|
||||
|
||||
const channel = "discord" as const;
|
||||
|
||||
@@ -104,8 +107,8 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
configuredScore: 2,
|
||||
unconfiguredScore: 1,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listDiscordAccountIds(cfg).some((accountId) => {
|
||||
const account = inspectDiscordAccount({ cfg, accountId });
|
||||
listDiscordSetupAccountIds(cfg).some((accountId) => {
|
||||
const account = inspectDiscordSetupAccount({ cfg, accountId });
|
||||
return account.configured;
|
||||
}),
|
||||
},
|
||||
@@ -122,7 +125,7 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
inputPrompt: "Enter Discord bot token",
|
||||
allowEnv: ({ accountId }: { accountId: string }) => accountId === DEFAULT_ACCOUNT_ID,
|
||||
inspect: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) => {
|
||||
const account = inspectDiscordAccount({ cfg, accountId });
|
||||
const account = inspectDiscordSetupAccount({ cfg, accountId });
|
||||
return {
|
||||
accountConfigured: account.configured,
|
||||
hasConfiguredValue: account.tokenStatus !== "missing",
|
||||
@@ -136,25 +139,24 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
},
|
||||
],
|
||||
groupAccess: createAccountScopedGroupAccessSection({
|
||||
channel,
|
||||
label: "Discord channels",
|
||||
placeholder: "My Server/#general, guildId/channelId, #support",
|
||||
currentPolicy: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
resolveDiscordAccount({ cfg, accountId }).config.groupPolicy ?? "allowlist",
|
||||
resolveDiscordSetupAccountConfig({ cfg, accountId }).config.groupPolicy ?? "allowlist",
|
||||
currentEntries: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Object.entries(resolveDiscordAccount({ cfg, accountId }).config.guilds ?? {}).flatMap(
|
||||
([guildKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) {
|
||||
const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey;
|
||||
return [input];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
|
||||
},
|
||||
),
|
||||
Object.entries(
|
||||
resolveDiscordSetupAccountConfig({ cfg, accountId }).config.guilds ?? {},
|
||||
).flatMap(([guildKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) {
|
||||
const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey;
|
||||
return [input];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
|
||||
}),
|
||||
updatePrompt: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Boolean(resolveDiscordAccount({ cfg, accountId }).config.guilds),
|
||||
Boolean(resolveDiscordSetupAccountConfig({ cfg, accountId }).config.guilds),
|
||||
resolveAllowlist: handlers.resolveGroupAllowlist,
|
||||
fallbackResolved: (entries) => entries.map((input) => ({ input, resolved: false })),
|
||||
applyAllowlist: ({
|
||||
@@ -168,7 +170,6 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
}) => setDiscordGuildChannelAllowlist(cfg, accountId, resolved as never),
|
||||
}),
|
||||
allowFrom: createAccountScopedAllowFromSection({
|
||||
channel,
|
||||
credentialInputKey: "token",
|
||||
helpTitle: "Discord allowlist",
|
||||
helpLines: [
|
||||
|
||||
436
extensions/discord/src/setup-runtime-helpers.ts
Normal file
436
extensions/discord/src/setup-runtime-helpers.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type {
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import {
|
||||
resolveDefaultDiscordSetupAccountId,
|
||||
resolveDiscordSetupAccountConfig,
|
||||
} from "./setup-account-state.js";
|
||||
|
||||
export function parseMentionOrPrefixedId(params: {
|
||||
value: string;
|
||||
mentionPattern: RegExp;
|
||||
prefixPattern?: RegExp;
|
||||
idPattern: RegExp;
|
||||
normalizeId?: (id: string) => string;
|
||||
}): string | null {
|
||||
const trimmed = params.value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const mentionMatch = trimmed.match(params.mentionPattern);
|
||||
if (mentionMatch?.[1]) {
|
||||
return params.normalizeId ? params.normalizeId(mentionMatch[1]) : mentionMatch[1];
|
||||
}
|
||||
if (params.prefixPattern?.test(trimmed)) {
|
||||
const stripped = trimmed.replace(params.prefixPattern, "").trim();
|
||||
if (!stripped || !params.idPattern.test(stripped)) {
|
||||
return null;
|
||||
}
|
||||
return params.normalizeId ? params.normalizeId(stripped) : stripped;
|
||||
}
|
||||
if (!params.idPattern.test(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
return params.normalizeId ? params.normalizeId(trimmed) : trimmed;
|
||||
}
|
||||
|
||||
function splitSetupEntries(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function mergeAllowFromEntries(
|
||||
current: Array<string | number> | null | undefined,
|
||||
additions: Array<string | number>,
|
||||
): string[] {
|
||||
const merged = [...(current ?? []), ...additions]
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean);
|
||||
return [...new Set(merged)];
|
||||
}
|
||||
|
||||
function patchDiscordChannelConfigForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
patch: Record<string, unknown>;
|
||||
}): OpenClawConfig {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const channelConfig = (params.cfg.channels?.discord as Record<string, unknown> | undefined) ?? {};
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
discord: {
|
||||
...channelConfig,
|
||||
...params.patch,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const accounts =
|
||||
(channelConfig.accounts as Record<string, Record<string, unknown>> | undefined) ?? {};
|
||||
const accountConfig = accounts[accountId] ?? {};
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
discord: {
|
||||
...channelConfig,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...accounts,
|
||||
[accountId]: {
|
||||
...accountConfig,
|
||||
...params.patch,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function setSetupChannelEnabled(
|
||||
cfg: OpenClawConfig,
|
||||
channel: string,
|
||||
enabled: boolean,
|
||||
): OpenClawConfig {
|
||||
const channelConfig = (cfg.channels?.[channel] as Record<string, unknown> | undefined) ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
[channel]: {
|
||||
...channelConfig,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function patchChannelConfigForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: "discord";
|
||||
accountId: string;
|
||||
patch: Record<string, unknown>;
|
||||
}): OpenClawConfig {
|
||||
return patchDiscordChannelConfigForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
patch: params.patch,
|
||||
});
|
||||
}
|
||||
|
||||
export function createLegacyCompatChannelDmPolicy(params: {
|
||||
label: string;
|
||||
channel: "discord";
|
||||
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
||||
}): ChannelSetupDmPolicy {
|
||||
return {
|
||||
label: params.label,
|
||||
channel: params.channel,
|
||||
policyKey: `channels.${params.channel}.dmPolicy`,
|
||||
allowFromKey: `channels.${params.channel}.allowFrom`,
|
||||
getCurrent: (cfg) =>
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
dmPolicy?: "open" | "pairing" | "allowlist";
|
||||
dm?: { policy?: "open" | "pairing" | "allowlist" };
|
||||
}
|
||||
| undefined
|
||||
)?.dmPolicy ??
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
dmPolicy?: "open" | "pairing" | "allowlist";
|
||||
dm?: { policy?: "open" | "pairing" | "allowlist" };
|
||||
}
|
||||
| undefined
|
||||
)?.dm?.policy ??
|
||||
"pairing",
|
||||
setPolicy: (cfg, policy) =>
|
||||
patchDiscordChannelConfigForAccount({
|
||||
cfg,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
patch: {
|
||||
dmPolicy: policy,
|
||||
...(policy === "open"
|
||||
? {
|
||||
allowFrom: [
|
||||
...new Set(
|
||||
[
|
||||
...(((
|
||||
cfg.channels?.discord as { allowFrom?: Array<string | number> } | undefined
|
||||
)?.allowFrom ?? []) as Array<string | number>),
|
||||
"*",
|
||||
]
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function noteChannelLookupFailure(params: {
|
||||
prompter: Pick<WizardPrompter, "note">;
|
||||
label: string;
|
||||
error: unknown;
|
||||
}) {
|
||||
await params.prompter.note(
|
||||
`Channel lookup failed; keeping entries as typed. ${String(params.error)}`,
|
||||
params.label,
|
||||
);
|
||||
}
|
||||
|
||||
export function createAccountScopedAllowFromSection(params: {
|
||||
credentialInputKey?: NonNullable<ChannelSetupWizard["allowFrom"]>["credentialInputKey"];
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
invalidWithoutCredentialNote: string;
|
||||
parseId: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseId"]>;
|
||||
resolveEntries: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]>;
|
||||
}): NonNullable<ChannelSetupWizard["allowFrom"]> {
|
||||
return {
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
...(params.credentialInputKey ? { credentialInputKey: params.credentialInputKey } : {}),
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
invalidWithoutCredentialNote: params.invalidWithoutCredentialNote,
|
||||
parseId: params.parseId,
|
||||
resolveEntries: params.resolveEntries,
|
||||
apply: ({ cfg, accountId, allowFrom }) =>
|
||||
patchDiscordChannelConfigForAccount({
|
||||
cfg,
|
||||
accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAccountScopedGroupAccessSection<TResolved>(params: {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
skipAllowlistEntries?: boolean;
|
||||
currentPolicy: NonNullable<ChannelSetupWizard["groupAccess"]>["currentPolicy"];
|
||||
currentEntries: NonNullable<ChannelSetupWizard["groupAccess"]>["currentEntries"];
|
||||
updatePrompt: NonNullable<ChannelSetupWizard["groupAccess"]>["updatePrompt"];
|
||||
resolveAllowlist?: NonNullable<
|
||||
NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]
|
||||
>;
|
||||
fallbackResolved: (entries: string[]) => TResolved;
|
||||
applyAllowlist: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
resolved: TResolved;
|
||||
}) => OpenClawConfig;
|
||||
}): NonNullable<ChannelSetupWizard["groupAccess"]> {
|
||||
return {
|
||||
label: params.label,
|
||||
placeholder: params.placeholder,
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
...(params.skipAllowlistEntries ? { skipAllowlistEntries: true } : {}),
|
||||
currentPolicy: params.currentPolicy,
|
||||
currentEntries: params.currentEntries,
|
||||
updatePrompt: params.updatePrompt,
|
||||
setPolicy: ({ cfg, accountId, policy }) =>
|
||||
patchDiscordChannelConfigForAccount({
|
||||
cfg,
|
||||
accountId,
|
||||
patch: { groupPolicy: policy },
|
||||
}),
|
||||
...(params.resolveAllowlist
|
||||
? {
|
||||
resolveAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => {
|
||||
try {
|
||||
return await params.resolveAllowlist!({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
});
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter,
|
||||
label: params.label,
|
||||
error,
|
||||
});
|
||||
return params.fallbackResolved(entries);
|
||||
}
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
applyAllowlist: ({ cfg, accountId, resolved }) =>
|
||||
params.applyAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
resolved: resolved as TResolved,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAllowlistSetupWizardProxy<TGroupResolved>(params: {
|
||||
loadWizard: () => Promise<ChannelSetupWizard>;
|
||||
createBase: (handlers: {
|
||||
promptAllowFrom: NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>;
|
||||
resolveAllowFromEntries: NonNullable<
|
||||
NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]
|
||||
>;
|
||||
resolveGroupAllowlist: NonNullable<
|
||||
NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>
|
||||
>;
|
||||
}) => ChannelSetupWizard;
|
||||
fallbackResolvedGroupAllowlist: (entries: string[]) => TGroupResolved;
|
||||
}) {
|
||||
return params.createBase({
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||
const wizard = await params.loadWizard();
|
||||
if (!wizard.dmPolicy?.promptAllowFrom) {
|
||||
return cfg;
|
||||
}
|
||||
return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId });
|
||||
},
|
||||
resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) => {
|
||||
const wizard = await params.loadWizard();
|
||||
if (!wizard.allowFrom) {
|
||||
return entries.map((input) => ({ input, resolved: false, id: null }));
|
||||
}
|
||||
return await wizard.allowFrom.resolveEntries({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
});
|
||||
},
|
||||
resolveGroupAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => {
|
||||
const wizard = await params.loadWizard();
|
||||
if (!wizard.groupAccess?.resolveAllowlist) {
|
||||
return params.fallbackResolvedGroupAllowlist(entries) as Awaited<
|
||||
ReturnType<
|
||||
NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>
|
||||
>
|
||||
>;
|
||||
}
|
||||
return (await wizard.groupAccess.resolveAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
})) as Awaited<
|
||||
ReturnType<NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>>
|
||||
>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveEntriesWithOptionalToken<TResult>(params: {
|
||||
token?: string | null;
|
||||
entries: string[];
|
||||
buildWithoutToken: (input: string) => TResult;
|
||||
resolveEntries: (params: { token: string; entries: string[] }) => Promise<TResult[]>;
|
||||
}): Promise<TResult[]> {
|
||||
const token = params.token?.trim();
|
||||
if (!token) {
|
||||
return params.entries.map(params.buildWithoutToken);
|
||||
}
|
||||
return await params.resolveEntries({
|
||||
token,
|
||||
entries: params.entries,
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptLegacyChannelAllowFromForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
noteTitle: string;
|
||||
noteLines: string[];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
parseId: (value: string) => string | null;
|
||||
invalidWithoutTokenNote: string;
|
||||
resolveEntries: (params: {
|
||||
token: string;
|
||||
entries: string[];
|
||||
}) => Promise<Array<{ input: string; resolved: boolean; id?: string | null }>>;
|
||||
resolveToken: (accountId: string) => string | null | undefined;
|
||||
resolveExisting: (accountId: string, cfg: OpenClawConfig) => Array<string | number>;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultDiscordSetupAccountId(params.cfg),
|
||||
);
|
||||
await params.prompter.note(params.noteLines.join("\n"), params.noteTitle);
|
||||
const token = params.resolveToken(accountId);
|
||||
const existing = params.resolveExisting(accountId, params.cfg);
|
||||
|
||||
while (true) {
|
||||
const entry = await params.prompter.text({
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
const parts = splitSetupEntries(String(entry));
|
||||
if (!token) {
|
||||
const ids = parts.map(params.parseId).filter(Boolean) as string[];
|
||||
if (ids.length !== parts.length) {
|
||||
await params.prompter.note(params.invalidWithoutTokenNote, params.noteTitle);
|
||||
continue;
|
||||
}
|
||||
return patchDiscordChannelConfigForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
patch: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: mergeAllowFromEntries(existing, ids),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const results = await params.resolveEntries({ token, entries: parts }).catch(() => null);
|
||||
if (!results) {
|
||||
await params.prompter.note("Failed to resolve usernames. Try again.", params.noteTitle);
|
||||
continue;
|
||||
}
|
||||
const unresolved = results.filter((result) => !result.resolved || !result.id);
|
||||
if (unresolved.length > 0) {
|
||||
await params.prompter.note(
|
||||
`Could not resolve: ${unresolved.map((result) => result.input).join(", ")}`,
|
||||
params.noteTitle,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
return patchDiscordChannelConfigForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
patch: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: mergeAllowFromEntries(
|
||||
existing,
|
||||
results.map((result) => result.id as string),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
import {
|
||||
resolveEntriesWithOptionalToken,
|
||||
type OpenClawConfig,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { resolveDefaultDiscordAccountId, resolveDiscordAccount } from "./accounts.js";
|
||||
import { resolveDiscordChannelAllowlist } from "./resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
|
||||
import {
|
||||
resolveDefaultDiscordSetupAccountId,
|
||||
resolveDiscordSetupAccountConfig,
|
||||
} from "./setup-account-state.js";
|
||||
import {
|
||||
createDiscordSetupWizardBase,
|
||||
DISCORD_TOKEN_HELP_LINES,
|
||||
parseDiscordAllowFromId,
|
||||
setDiscordGuildChannelAllowlist,
|
||||
} from "./setup-core.js";
|
||||
import {
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
resolveEntriesWithOptionalToken,
|
||||
} from "./setup-runtime-helpers.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
const channel = "discord" as const;
|
||||
|
||||
@@ -48,13 +54,8 @@ async function promptDiscordAllowFrom(params: {
|
||||
}): Promise<OpenClawConfig> {
|
||||
return await promptLegacyChannelAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
prompter: params.prompter,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultDiscordAccountId(params.cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
resolveExisting: (account) => account.config.allowFrom ?? account.config.dm?.allowFrom ?? [],
|
||||
resolveToken: (account) => account.token,
|
||||
noteTitle: "Discord allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Discord DMs by username (we resolve to user ids).",
|
||||
@@ -69,6 +70,11 @@ async function promptDiscordAllowFrom(params: {
|
||||
placeholder: "@alice, 123456789012345678",
|
||||
parseId: parseDiscordAllowFromId,
|
||||
invalidWithoutTokenNote: "Bot token missing; use numeric user ids (or mention form) only.",
|
||||
resolveExisting: (accountId, cfg) => {
|
||||
const account = resolveDiscordSetupAccountConfig({ cfg, accountId }).config;
|
||||
return account.allowFrom ?? account.dm?.allowFrom ?? [];
|
||||
},
|
||||
resolveToken: (accountId) => resolveDiscordToken(params.cfg, { accountId }).token,
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
(
|
||||
await resolveDiscordUserAllowlist({
|
||||
@@ -91,7 +97,7 @@ async function resolveDiscordGroupAllowlist(params: {
|
||||
}) {
|
||||
return await resolveEntriesWithOptionalToken({
|
||||
token:
|
||||
resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }).token ||
|
||||
resolveDiscordToken(params.cfg, { accountId: params.accountId }).token ||
|
||||
(typeof params.credentialValues.token === "string" ? params.credentialValues.token : ""),
|
||||
entries: params.entries,
|
||||
buildWithoutToken: (input) => ({
|
||||
@@ -111,7 +117,7 @@ export const discordSetupWizard: ChannelSetupWizard = createDiscordSetupWizardBa
|
||||
resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) =>
|
||||
await resolveDiscordAllowFromEntries({
|
||||
token:
|
||||
resolveDiscordAccount({ cfg, accountId }).token ||
|
||||
resolveDiscordToken(cfg, { accountId }).token ||
|
||||
(typeof credentialValues.token === "string" ? credentialValues.token : ""),
|
||||
entries,
|
||||
}),
|
||||
|
||||
@@ -100,7 +100,6 @@ function createHandlerHarness() {
|
||||
mediaMaxBytes: 5 * 1024 * 1024,
|
||||
startupMs: Date.now() - 120_000,
|
||||
startupGraceMs: 60_000,
|
||||
dropPreStartupMessages: false,
|
||||
directTracker: {
|
||||
isDirectMessage: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
|
||||
@@ -590,7 +590,6 @@ describe("matrix monitor handler pairing account scope", () => {
|
||||
mediaMaxBytes: 10_000_000,
|
||||
startupMs: 0,
|
||||
startupGraceMs: 0,
|
||||
dropPreStartupMessages: false,
|
||||
directTracker: {
|
||||
isDirectMessage: async () => false,
|
||||
},
|
||||
|
||||
@@ -115,7 +115,6 @@ describe("createMatrixRoomMessageHandler thread root media", () => {
|
||||
mediaMaxBytes: 5 * 1024 * 1024,
|
||||
startupMs: Date.now() - 120_000,
|
||||
startupGraceMs: 60_000,
|
||||
dropPreStartupMessages: false,
|
||||
directTracker: {
|
||||
isDirectMessage: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
|
||||
@@ -77,6 +77,14 @@
|
||||
"types": "./dist/plugin-sdk/setup.d.ts",
|
||||
"default": "./dist/plugin-sdk/setup.js"
|
||||
},
|
||||
"./plugin-sdk/setup-adapter-runtime": {
|
||||
"types": "./dist/plugin-sdk/setup-adapter-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/setup-adapter-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/setup-runtime": {
|
||||
"types": "./dist/plugin-sdk/setup-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/setup-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/channel-setup": {
|
||||
"types": "./dist/plugin-sdk/channel-setup.d.ts",
|
||||
"default": "./dist/plugin-sdk/channel-setup.js"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"runtime",
|
||||
"runtime-env",
|
||||
"setup",
|
||||
"setup-adapter-runtime",
|
||||
"setup-runtime",
|
||||
"channel-setup",
|
||||
"setup-tools",
|
||||
"config-runtime",
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as acpSessionManager from "../acp/control-plane/manager.js";
|
||||
import type {
|
||||
AcpCloseSessionInput,
|
||||
AcpInitializeSessionInput,
|
||||
} from "../acp/control-plane/manager.types.js";
|
||||
import type { AcpInitializeSessionInput } from "../acp/control-plane/manager.types.js";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect, vi } from "vitest";
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
createThreadBindingManager as createDiscordThreadBindingManager,
|
||||
} from "../../../../extensions/discord/runtime-api.js";
|
||||
import { createFeishuThreadBindingManager } from "../../../../extensions/feishu/api.js";
|
||||
import { createMatrixThreadBindingManager } from "../../../../extensions/matrix/api.js";
|
||||
import {
|
||||
createMatrixThreadBindingManager,
|
||||
resetMatrixThreadBindingsForTests,
|
||||
} from "../../../../extensions/matrix/api.js";
|
||||
import { setMatrixRuntime } from "../../../../extensions/matrix/index.js";
|
||||
import { createTelegramThreadBindingManager } from "../../../../extensions/telegram/runtime-api.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
@@ -181,6 +185,12 @@ function expectClearedSessionBinding(params: {
|
||||
|
||||
const telegramDescribeMessageToolMock = vi.fn();
|
||||
const discordDescribeMessageToolMock = vi.fn();
|
||||
const sendMessageMatrixMock = vi.hoisted(() =>
|
||||
vi.fn(async (to: string, _message: string, opts?: { threadId?: string }) => ({
|
||||
messageId: opts?.threadId ? "$matrix-thread" : "$matrix-root",
|
||||
roomId: to.replace(/^room:/, ""),
|
||||
})),
|
||||
);
|
||||
|
||||
bundledChannelRuntimeSetters.setTelegramRuntime({
|
||||
channel: {
|
||||
@@ -213,6 +223,48 @@ bundledChannelRuntimeSetters.setLineRuntime({
|
||||
},
|
||||
} as never);
|
||||
|
||||
vi.mock("../../../../extensions/matrix/src/matrix/send.js", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("../../../../extensions/matrix/src/matrix/send.js")
|
||||
>("../../../../extensions/matrix/src/matrix/send.js");
|
||||
return {
|
||||
...actual,
|
||||
sendMessageMatrix: sendMessageMatrixMock,
|
||||
};
|
||||
});
|
||||
|
||||
const matrixSessionBindingStateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "openclaw-matrix-session-binding-contract-"),
|
||||
);
|
||||
const matrixSessionBindingAuth = {
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
} as const;
|
||||
|
||||
function resetMatrixSessionBindingStateDir() {
|
||||
fs.rmSync(matrixSessionBindingStateDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(matrixSessionBindingStateDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function createContractMatrixThreadBindingManager() {
|
||||
resetMatrixSessionBindingStateDir();
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: () => matrixSessionBindingStateDir,
|
||||
},
|
||||
} as never);
|
||||
return await createMatrixThreadBindingManager({
|
||||
accountId: matrixSessionBindingAuth.accountId,
|
||||
auth: matrixSessionBindingAuth,
|
||||
client: {} as never,
|
||||
idleTimeoutMs: 24 * 60 * 60 * 1000,
|
||||
maxAgeMs: 0,
|
||||
enableSweeper: false,
|
||||
});
|
||||
}
|
||||
|
||||
export const pluginContractRegistry: PluginContractEntry[] = bundledChannelPlugins.map(
|
||||
(plugin) => ({
|
||||
id: plugin.id,
|
||||
@@ -595,24 +647,6 @@ const baseSessionBindingCfg = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
async function createContractMatrixThreadBindingManager() {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-contract-thread-bindings-"));
|
||||
return await createMatrixThreadBindingManager({
|
||||
accountId: "ops",
|
||||
auth: {
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
},
|
||||
client: {} as never,
|
||||
stateDir,
|
||||
idleTimeoutMs: 24 * 60 * 60 * 1000,
|
||||
maxAgeMs: 0,
|
||||
enableSweeper: false,
|
||||
});
|
||||
}
|
||||
|
||||
export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
|
||||
{
|
||||
id: "discord",
|
||||
@@ -744,47 +778,43 @@ export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
|
||||
await createContractMatrixThreadBindingManager();
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "matrix",
|
||||
accountId: "ops",
|
||||
accountId: matrixSessionBindingAuth.accountId,
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
await createContractMatrixThreadBindingManager();
|
||||
const service = getSessionBindingService();
|
||||
const binding = await service.bind({
|
||||
targetSessionKey: "agent:matrix:subagent:child-1",
|
||||
targetSessionKey: "agent:matrix:child:thread-1",
|
||||
targetKind: "subagent",
|
||||
conversation: {
|
||||
channel: "matrix",
|
||||
accountId: "ops",
|
||||
conversationId: "!room:example",
|
||||
accountId: matrixSessionBindingAuth.accountId,
|
||||
conversationId: "$thread",
|
||||
parentConversationId: "!room:example",
|
||||
},
|
||||
placement: "child",
|
||||
placement: "current",
|
||||
metadata: {
|
||||
label: "codex-matrix",
|
||||
introText: "intro root",
|
||||
},
|
||||
});
|
||||
expectResolvedSessionBinding({
|
||||
channel: "matrix",
|
||||
accountId: "ops",
|
||||
conversationId: "$root",
|
||||
parentConversationId: "!room:example",
|
||||
targetSessionKey: "agent:matrix:subagent:child-1",
|
||||
accountId: matrixSessionBindingAuth.accountId,
|
||||
conversationId: "$thread",
|
||||
targetSessionKey: "agent:matrix:child:thread-1",
|
||||
});
|
||||
return binding;
|
||||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
const manager = await createContractMatrixThreadBindingManager();
|
||||
manager.stop();
|
||||
expect(
|
||||
getSessionBindingService().resolveByConversation({
|
||||
channel: "matrix",
|
||||
accountId: "ops",
|
||||
conversationId: "$root",
|
||||
parentConversationId: "!room:example",
|
||||
}),
|
||||
).toBeNull();
|
||||
resetMatrixThreadBindingsForTests();
|
||||
resetMatrixSessionBindingStateDir();
|
||||
expectClearedSessionBinding({
|
||||
channel: "matrix",
|
||||
accountId: matrixSessionBindingAuth.accountId,
|
||||
conversationId: "$thread",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -485,7 +485,7 @@ export function installSessionBindingContractSuite(params: {
|
||||
expectedCapabilities: SessionBindingCapabilities;
|
||||
}) {
|
||||
it("registers the expected session binding capabilities", async () => {
|
||||
expect(await params.getCapabilities()).toEqual(params.expectedCapabilities);
|
||||
expect(await Promise.resolve(params.getCapabilities())).toEqual(params.expectedCapabilities);
|
||||
});
|
||||
|
||||
it("binds and resolves a session binding through the shared service", async () => {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
|
||||
import type { SecretInput } from "../../config/types.secrets.js";
|
||||
import {
|
||||
promptSecretRefForSetup,
|
||||
resolveSecretInputModeForEnvSelection,
|
||||
} from "../../plugins/provider-auth-input.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
import {
|
||||
@@ -18,6 +14,15 @@ import type {
|
||||
} from "./setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard, ChannelSetupWizardAllowFromEntry } from "./setup-wizard.js";
|
||||
|
||||
let providerAuthInputPromise:
|
||||
| Promise<typeof import("../../plugins/provider-auth-input.js")>
|
||||
| undefined;
|
||||
|
||||
function loadProviderAuthInput() {
|
||||
providerAuthInputPromise ??= import("../../plugins/provider-auth-input.js");
|
||||
return providerAuthInputPromise;
|
||||
}
|
||||
|
||||
export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => {
|
||||
const existingIds = params.listAccountIds(params.cfg);
|
||||
const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID;
|
||||
@@ -994,6 +999,8 @@ export async function promptSingleChannelSecretInput(params: {
|
||||
inputPrompt: string;
|
||||
preferredEnvVar?: string;
|
||||
}): Promise<SingleChannelSecretInputPromptResult> {
|
||||
const { promptSecretRefForSetup, resolveSecretInputModeForEnvSelection } =
|
||||
await loadProviderAuthInput();
|
||||
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
||||
prompter: params.prompter as WizardPrompter,
|
||||
explicitMode: params.secretInputMode,
|
||||
|
||||
@@ -314,6 +314,12 @@ function isUnsupportedSecretsResolveError(err: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isDirectRuntimeWebTargetPath(path: string): boolean {
|
||||
return (
|
||||
path === "tools.web.fetch.firecrawl.apiKey" || /^tools\.web\.search\.[^.]+\.apiKey$/.test(path)
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveCommandSecretRefsLocally(params: {
|
||||
config: OpenClawConfig;
|
||||
commandName: string;
|
||||
@@ -329,12 +335,22 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
env: process.env,
|
||||
});
|
||||
const localResolutionDiagnostics: string[] = [];
|
||||
const discoveredTargets = discoverConfigSecretTargetsByIds(sourceConfig, params.targetIds).filter(
|
||||
(target) => !params.allowedPaths || params.allowedPaths.has(target.path),
|
||||
);
|
||||
const runtimeWebTargets = discoveredTargets.filter((target) =>
|
||||
targetsRuntimeWebPath(target.path),
|
||||
);
|
||||
collectConfigAssignments({
|
||||
config: structuredClone(params.config),
|
||||
context,
|
||||
});
|
||||
if (
|
||||
targetsRuntimeWebResolution({ targetIds: params.targetIds, allowedPaths: params.allowedPaths })
|
||||
targetsRuntimeWebResolution({
|
||||
targetIds: params.targetIds,
|
||||
allowedPaths: params.allowedPaths,
|
||||
}) &&
|
||||
!runtimeWebTargets.every((target) => isDirectRuntimeWebTargetPath(target.path))
|
||||
) {
|
||||
try {
|
||||
await resolveRuntimeWebTools({
|
||||
@@ -359,13 +375,7 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
);
|
||||
const runtimeWebActivePaths = new Set<string>();
|
||||
const runtimeWebInactiveDiagnostics: string[] = [];
|
||||
for (const target of discoverConfigSecretTargetsByIds(sourceConfig, params.targetIds)) {
|
||||
if (!targetsRuntimeWebPath(target.path)) {
|
||||
continue;
|
||||
}
|
||||
if (params.allowedPaths && !params.allowedPaths.has(target.path)) {
|
||||
continue;
|
||||
}
|
||||
for (const target of runtimeWebTargets) {
|
||||
const runtimeState = classifyRuntimeWebTargetPathState({
|
||||
config: sourceConfig,
|
||||
path: target.path,
|
||||
@@ -390,10 +400,7 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
.filter((warning) => !params.allowedPaths || params.allowedPaths.has(warning.path))
|
||||
.map((warning) => warning.message);
|
||||
const activePaths = new Set(context.assignments.map((assignment) => assignment.path));
|
||||
for (const target of discoverConfigSecretTargetsByIds(sourceConfig, params.targetIds)) {
|
||||
if (params.allowedPaths && !params.allowedPaths.has(target.path)) {
|
||||
continue;
|
||||
}
|
||||
for (const target of discoveredTargets) {
|
||||
await resolveTargetSecretLocally({
|
||||
target,
|
||||
sourceConfig,
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type {
|
||||
ChannelDirectoryEntryKind,
|
||||
ChannelMessagingAdapter,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
createChannelTestPluginBase,
|
||||
createTestRegistry,
|
||||
} from "../../test-utils/channel-plugins.js";
|
||||
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
|
||||
import { runMessageAction } from "./message-action-runner.js";
|
||||
|
||||
const slackConfig = {
|
||||
@@ -52,48 +61,112 @@ const runDrySend = (params: {
|
||||
action: "send",
|
||||
});
|
||||
|
||||
const createDryRunPlugin = (id: "slack" | "whatsapp" | "telegram" | "imessage"): ChannelPlugin => {
|
||||
const plugin = createOutboundTestPlugin({
|
||||
id,
|
||||
outbound: {} as never,
|
||||
});
|
||||
type ResolvedTestTarget = { to: string; kind: ChannelDirectoryEntryKind };
|
||||
|
||||
const resolveTarget: NonNullable<
|
||||
NonNullable<ChannelPlugin["messaging"]>["targetResolver"]
|
||||
>["resolveTarget"] = async ({ input, normalized }) => {
|
||||
if (id === "slack") {
|
||||
const raw = input.replace(/^#/, "");
|
||||
return { to: `channel:${raw}`, kind: "group", source: "normalized" };
|
||||
}
|
||||
if (id === "telegram") {
|
||||
return { to: `group:${normalized || input}`, kind: "group", source: "normalized" };
|
||||
}
|
||||
if (id === "whatsapp") {
|
||||
return { to: `group:${normalized || input}`, kind: "group", source: "normalized" };
|
||||
}
|
||||
return { to: normalized || input, kind: "user", source: "normalized" };
|
||||
const directOutbound: ChannelOutboundAdapter = { deliveryMode: "direct" };
|
||||
|
||||
function normalizeSlackTarget(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
if (trimmed.startsWith("#")) {
|
||||
return trimmed.slice(1).trim();
|
||||
}
|
||||
if (/^channel:/i.test(trimmed)) {
|
||||
return trimmed.replace(/^channel:/i, "").trim();
|
||||
}
|
||||
if (/^user:/i.test(trimmed)) {
|
||||
return trimmed.replace(/^user:/i, "").trim();
|
||||
}
|
||||
const mention = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
||||
if (mention?.[1]) {
|
||||
return mention[1];
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function createConfiguredTestPlugin(params: {
|
||||
id: "slack" | "telegram" | "whatsapp";
|
||||
isConfigured: (cfg: OpenClawConfig) => boolean;
|
||||
normalizeTarget: (raw: string) => string | undefined;
|
||||
resolveTarget: (input: string) => ResolvedTestTarget | null;
|
||||
}): ChannelPlugin {
|
||||
const messaging: ChannelMessagingAdapter = {
|
||||
normalizeTarget: params.normalizeTarget,
|
||||
targetResolver: {
|
||||
looksLikeId: (raw) => Boolean(params.resolveTarget(raw.trim())),
|
||||
hint: "<id>",
|
||||
resolveTarget: async (resolverParams) => {
|
||||
const resolved = params.resolveTarget(resolverParams.input);
|
||||
return resolved ? { ...resolved, source: "normalized" } : null;
|
||||
},
|
||||
},
|
||||
inferTargetChatType: (inferParams) =>
|
||||
params.resolveTarget(inferParams.to)?.kind === "user" ? "direct" : "group",
|
||||
};
|
||||
|
||||
return {
|
||||
...plugin,
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
messaging: {
|
||||
inferTargetChatType: ({ to }) => {
|
||||
if (id === "imessage" && to.startsWith("imessage:")) {
|
||||
return "direct";
|
||||
}
|
||||
return "group";
|
||||
...createChannelTestPluginBase({
|
||||
id: params.id,
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ enabled: true }),
|
||||
isConfigured: (_account, cfg) => params.isConfigured(cfg),
|
||||
},
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
resolveTarget,
|
||||
},
|
||||
},
|
||||
}),
|
||||
outbound: directOutbound,
|
||||
messaging,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const slackTestPlugin = createConfiguredTestPlugin({
|
||||
id: "slack",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.slack?.botToken?.trim()),
|
||||
normalizeTarget: (raw) => normalizeSlackTarget(raw) || undefined,
|
||||
resolveTarget: (input) => {
|
||||
const normalized = normalizeSlackTarget(input);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (/^[A-Z0-9]+$/i.test(normalized)) {
|
||||
const kind = /^U/i.test(normalized) ? "user" : "group";
|
||||
return { to: normalized, kind };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
const telegramTestPlugin = createConfiguredTestPlugin({
|
||||
id: "telegram",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.telegram?.botToken?.trim()),
|
||||
normalizeTarget: (raw) => raw.trim() || undefined,
|
||||
resolveTarget: (input) => {
|
||||
const normalized = input.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
to: normalized.replace(/^telegram:/i, ""),
|
||||
kind: normalized.startsWith("@") ? "user" : "group",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const whatsappTestPlugin = createConfiguredTestPlugin({
|
||||
id: "whatsapp",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.whatsapp),
|
||||
normalizeTarget: (raw) => raw.trim() || undefined,
|
||||
resolveTarget: (input) => {
|
||||
const normalized = input.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
to: normalized,
|
||||
kind: normalized.endsWith("@g.us") ? "group" : "user",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
describe("runMessageAction context isolation", () => {
|
||||
beforeEach(() => {
|
||||
@@ -102,22 +175,22 @@ describe("runMessageAction context isolation", () => {
|
||||
{
|
||||
pluginId: "slack",
|
||||
source: "test",
|
||||
plugin: createDryRunPlugin("slack"),
|
||||
plugin: slackTestPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
source: "test",
|
||||
plugin: createDryRunPlugin("whatsapp"),
|
||||
plugin: whatsappTestPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "telegram",
|
||||
source: "test",
|
||||
plugin: createDryRunPlugin("telegram"),
|
||||
plugin: telegramTestPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "imessage",
|
||||
source: "test",
|
||||
plugin: createDryRunPlugin("imessage"),
|
||||
plugin: createIMessageTestPlugin(),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -58,7 +58,6 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
|
||||
ban: "none",
|
||||
"set-profile": "none",
|
||||
"set-presence": "none",
|
||||
"set-profile": "none",
|
||||
"download-file": "none",
|
||||
};
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ export type {
|
||||
OpenClawPluginDefinition,
|
||||
PluginCommandContext,
|
||||
PluginLogger,
|
||||
PluginCommandContext,
|
||||
PluginInteractiveTelegramHandlerContext,
|
||||
} from "../plugins/types.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
1
src/plugin-sdk/setup-adapter-runtime.ts
Normal file
1
src/plugin-sdk/setup-adapter-runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createEnvPatchedAccountSetupAdapter } from "../channels/plugins/setup-helpers.js";
|
||||
25
src/plugin-sdk/setup-runtime.ts
Normal file
25
src/plugin-sdk/setup-runtime.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js";
|
||||
export type { ChannelSetupDmPolicy } from "../channels/plugins/setup-wizard-types.js";
|
||||
export type {
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardAllowFromEntry,
|
||||
} from "../channels/plugins/setup-wizard.js";
|
||||
|
||||
export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
|
||||
export { createEnvPatchedAccountSetupAdapter } from "../channels/plugins/setup-helpers.js";
|
||||
|
||||
export {
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
resolveEntriesWithOptionalToken,
|
||||
setSetupChannelEnabled,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
|
||||
export { createAllowlistSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";
|
||||
Reference in New Issue
Block a user