mirror of https://github.com/openclaw/openclaw.git
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
|
|
import {
|
|
createScopedAccountReplyToModeResolver,
|
|
createTopLevelChannelReplyToModeResolver,
|
|
} from "../channels/plugins/threading-helpers.js";
|
|
import type {
|
|
ChannelOutboundAdapter,
|
|
ChannelPairingAdapter,
|
|
ChannelSecurityAdapter,
|
|
} from "../channels/plugins/types.adapters.js";
|
|
import type {
|
|
ChannelMessagingAdapter,
|
|
ChannelOutboundSessionRoute,
|
|
ChannelPollResult,
|
|
ChannelThreadingAdapter,
|
|
} from "../channels/plugins/types.core.js";
|
|
import type { ChannelConfigUiHint, ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import type { ReplyToMode } from "../config/types.base.js";
|
|
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
|
|
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
|
|
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
|
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
|
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
|
|
|
|
export type { ChannelConfigUiHint, ChannelPlugin };
|
|
export type { OpenClawConfig };
|
|
export type { PluginRuntime };
|
|
|
|
export type ChannelOutboundSessionRouteParams = Parameters<
|
|
NonNullable<ChannelMessagingAdapter["resolveOutboundSessionRoute"]>
|
|
>[0];
|
|
|
|
type DefineChannelPluginEntryOptions<TPlugin = ChannelPlugin> = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
plugin: TPlugin;
|
|
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
|
|
setRuntime?: (runtime: PluginRuntime) => void;
|
|
registerCliMetadata?: (api: OpenClawPluginApi) => void;
|
|
registerFull?: (api: OpenClawPluginApi) => void;
|
|
};
|
|
|
|
type DefinedChannelPluginEntry<TPlugin> = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
configSchema: OpenClawPluginConfigSchema;
|
|
register: (api: OpenClawPluginApi) => void;
|
|
channelPlugin: TPlugin;
|
|
setChannelRuntime?: (runtime: PluginRuntime) => void;
|
|
};
|
|
|
|
type ChatChannelPluginBase<TResolvedAccount, Probe, Audit> = Omit<
|
|
ChannelPlugin<TResolvedAccount, Probe, Audit>,
|
|
"security" | "pairing" | "threading" | "outbound"
|
|
> &
|
|
Partial<
|
|
Pick<
|
|
ChannelPlugin<TResolvedAccount, Probe, Audit>,
|
|
"security" | "pairing" | "threading" | "outbound"
|
|
>
|
|
>;
|
|
|
|
type ChatChannelSecurityOptions<TResolvedAccount extends { accountId?: string | null }> = {
|
|
dm: {
|
|
channelKey: string;
|
|
resolvePolicy: (account: TResolvedAccount) => string | null | undefined;
|
|
resolveAllowFrom: (account: TResolvedAccount) => Array<string | number> | null | undefined;
|
|
resolveFallbackAccountId?: (account: TResolvedAccount) => string | null | undefined;
|
|
defaultPolicy?: string;
|
|
allowFromPathSuffix?: string;
|
|
policyPathSuffix?: string;
|
|
approveChannelId?: string;
|
|
approveHint?: string;
|
|
normalizeEntry?: (raw: string) => string;
|
|
};
|
|
collectWarnings?: ChannelSecurityAdapter<TResolvedAccount>["collectWarnings"];
|
|
collectAuditFindings?: ChannelSecurityAdapter<TResolvedAccount>["collectAuditFindings"];
|
|
};
|
|
|
|
type ChatChannelPairingOptions = {
|
|
text: {
|
|
idLabel: string;
|
|
message: string;
|
|
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
|
|
notify: (
|
|
params: Parameters<NonNullable<ChannelPairingAdapter["notifyApproval"]>>[0] & {
|
|
message: string;
|
|
},
|
|
) => Promise<void> | void;
|
|
};
|
|
};
|
|
|
|
type ChatChannelThreadingReplyModeOptions<TResolvedAccount> =
|
|
| { topLevelReplyToMode: string }
|
|
| {
|
|
scopedAccountReplyToMode: {
|
|
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => TResolvedAccount;
|
|
resolveReplyToMode: (
|
|
account: TResolvedAccount,
|
|
chatType?: string | null,
|
|
) => ReplyToMode | null | undefined;
|
|
fallback?: ReplyToMode;
|
|
};
|
|
}
|
|
| {
|
|
resolveReplyToMode: NonNullable<ChannelThreadingAdapter["resolveReplyToMode"]>;
|
|
};
|
|
|
|
type ChatChannelThreadingOptions<TResolvedAccount> =
|
|
ChatChannelThreadingReplyModeOptions<TResolvedAccount> &
|
|
Omit<ChannelThreadingAdapter, "resolveReplyToMode">;
|
|
|
|
type ChatChannelAttachedOutboundOptions = {
|
|
base: Omit<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
|
|
attachedResults: {
|
|
channel: string;
|
|
sendText?: (
|
|
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0],
|
|
) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
|
sendMedia?: (
|
|
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendMedia"]>>[0],
|
|
) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
|
sendPoll?: (
|
|
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0],
|
|
) => MaybePromise<Omit<ChannelPollResult, "channel">>;
|
|
};
|
|
};
|
|
|
|
type MaybePromise<T> = T | Promise<T>;
|
|
|
|
function createInlineTextPairingAdapter(params: {
|
|
idLabel: string;
|
|
message: string;
|
|
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
|
|
notify: (
|
|
params: Parameters<NonNullable<ChannelPairingAdapter["notifyApproval"]>>[0] & {
|
|
message: string;
|
|
},
|
|
) => Promise<void> | void;
|
|
}): ChannelPairingAdapter {
|
|
return {
|
|
idLabel: params.idLabel,
|
|
normalizeAllowEntry: params.normalizeAllowEntry,
|
|
notifyApproval: async (ctx) => {
|
|
await params.notify({
|
|
...ctx,
|
|
message: params.message,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
function createInlineAttachedChannelResultAdapter(
|
|
params: ChatChannelAttachedOutboundOptions["attachedResults"],
|
|
) {
|
|
return {
|
|
sendText: params.sendText
|
|
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0]) => ({
|
|
channel: params.channel,
|
|
...(await params.sendText!(ctx)),
|
|
})
|
|
: undefined,
|
|
sendMedia: params.sendMedia
|
|
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendMedia"]>>[0]) => ({
|
|
channel: params.channel,
|
|
...(await params.sendMedia!(ctx)),
|
|
})
|
|
: undefined,
|
|
sendPoll: params.sendPoll
|
|
? async (ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0]) => ({
|
|
channel: params.channel,
|
|
...(await params.sendPoll!(ctx)),
|
|
})
|
|
: undefined,
|
|
} satisfies Pick<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
|
|
}
|
|
|
|
function resolveChatChannelSecurity<TResolvedAccount extends { accountId?: string | null }>(
|
|
security:
|
|
| ChannelSecurityAdapter<TResolvedAccount>
|
|
| ChatChannelSecurityOptions<TResolvedAccount>
|
|
| undefined,
|
|
): ChannelSecurityAdapter<TResolvedAccount> | undefined {
|
|
if (!security) {
|
|
return undefined;
|
|
}
|
|
if (!("dm" in security)) {
|
|
return security;
|
|
}
|
|
return {
|
|
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
|
buildAccountScopedDmSecurityPolicy({
|
|
cfg,
|
|
channelKey: security.dm.channelKey,
|
|
accountId,
|
|
fallbackAccountId: security.dm.resolveFallbackAccountId?.(account) ?? account.accountId,
|
|
policy: security.dm.resolvePolicy(account),
|
|
allowFrom: security.dm.resolveAllowFrom(account) ?? [],
|
|
defaultPolicy: security.dm.defaultPolicy,
|
|
allowFromPathSuffix: security.dm.allowFromPathSuffix,
|
|
policyPathSuffix: security.dm.policyPathSuffix,
|
|
approveChannelId: security.dm.approveChannelId,
|
|
approveHint: security.dm.approveHint,
|
|
normalizeEntry: security.dm.normalizeEntry,
|
|
}),
|
|
...(security.collectWarnings ? { collectWarnings: security.collectWarnings } : {}),
|
|
...(security.collectAuditFindings
|
|
? { collectAuditFindings: security.collectAuditFindings }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
function resolveChatChannelPairing(
|
|
pairing: ChannelPairingAdapter | ChatChannelPairingOptions | undefined,
|
|
): ChannelPairingAdapter | undefined {
|
|
if (!pairing) {
|
|
return undefined;
|
|
}
|
|
if (!("text" in pairing)) {
|
|
return pairing;
|
|
}
|
|
return createInlineTextPairingAdapter(pairing.text);
|
|
}
|
|
|
|
function resolveChatChannelThreading<TResolvedAccount>(
|
|
threading: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount> | undefined,
|
|
): ChannelThreadingAdapter | undefined {
|
|
if (!threading) {
|
|
return undefined;
|
|
}
|
|
if (!("topLevelReplyToMode" in threading) && !("scopedAccountReplyToMode" in threading)) {
|
|
return threading;
|
|
}
|
|
|
|
let resolveReplyToMode: ChannelThreadingAdapter["resolveReplyToMode"];
|
|
if ("topLevelReplyToMode" in threading) {
|
|
resolveReplyToMode = createTopLevelChannelReplyToModeResolver(threading.topLevelReplyToMode);
|
|
} else {
|
|
resolveReplyToMode = createScopedAccountReplyToModeResolver<TResolvedAccount>(
|
|
threading.scopedAccountReplyToMode,
|
|
);
|
|
}
|
|
|
|
return {
|
|
...threading,
|
|
resolveReplyToMode,
|
|
};
|
|
}
|
|
|
|
function resolveChatChannelOutbound(
|
|
outbound: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions | undefined,
|
|
): ChannelOutboundAdapter | undefined {
|
|
if (!outbound) {
|
|
return undefined;
|
|
}
|
|
if (!("attachedResults" in outbound)) {
|
|
return outbound;
|
|
}
|
|
return {
|
|
...outbound.base,
|
|
...createInlineAttachedChannelResultAdapter(outbound.attachedResults),
|
|
};
|
|
}
|
|
|
|
export function defineChannelPluginEntry<TPlugin>({
|
|
id,
|
|
name,
|
|
description,
|
|
plugin,
|
|
configSchema = emptyPluginConfigSchema,
|
|
setRuntime,
|
|
registerCliMetadata,
|
|
registerFull,
|
|
}: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin> {
|
|
const resolvedConfigSchema = typeof configSchema === "function" ? configSchema() : configSchema;
|
|
const entry = {
|
|
id,
|
|
name,
|
|
description,
|
|
configSchema: resolvedConfigSchema,
|
|
register(api: OpenClawPluginApi) {
|
|
if (api.registrationMode === "cli-metadata") {
|
|
registerCliMetadata?.(api);
|
|
return;
|
|
}
|
|
setRuntime?.(api.runtime);
|
|
api.registerChannel({ plugin: plugin as ChannelPlugin });
|
|
if (api.registrationMode !== "full") {
|
|
return;
|
|
}
|
|
registerCliMetadata?.(api);
|
|
registerFull?.(api);
|
|
},
|
|
};
|
|
return {
|
|
...entry,
|
|
channelPlugin: plugin,
|
|
...(setRuntime ? { setChannelRuntime: setRuntime } : {}),
|
|
};
|
|
}
|
|
|
|
export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin) {
|
|
return { plugin };
|
|
}
|
|
|
|
export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string {
|
|
const trimmed = raw.trim();
|
|
for (const provider of providers) {
|
|
const prefix = `${provider.toLowerCase()}:`;
|
|
if (trimmed.toLowerCase().startsWith(prefix)) {
|
|
return trimmed.slice(prefix.length).trim();
|
|
}
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
export function stripTargetKindPrefix(raw: string): string {
|
|
return raw.replace(/^(user|channel|group|conversation|room|dm):/i, "").trim();
|
|
}
|
|
|
|
export function buildChannelOutboundSessionRoute(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
channel: string;
|
|
accountId?: string | null;
|
|
peer: { kind: "direct" | "group" | "channel"; id: string };
|
|
chatType: "direct" | "group" | "channel";
|
|
from: string;
|
|
to: string;
|
|
threadId?: string | number;
|
|
}): ChannelOutboundSessionRoute {
|
|
const baseSessionKey = buildOutboundBaseSessionKey({
|
|
cfg: params.cfg,
|
|
agentId: params.agentId,
|
|
channel: params.channel,
|
|
accountId: params.accountId,
|
|
peer: params.peer,
|
|
});
|
|
return {
|
|
sessionKey: baseSessionKey,
|
|
baseSessionKey,
|
|
peer: params.peer,
|
|
chatType: params.chatType,
|
|
from: params.from,
|
|
to: params.to,
|
|
...(params.threadId !== undefined ? { threadId: params.threadId } : {}),
|
|
};
|
|
}
|
|
|
|
export function createChatChannelPlugin<
|
|
TResolvedAccount extends { accountId?: string | null },
|
|
Probe = unknown,
|
|
Audit = unknown,
|
|
>(params: {
|
|
base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>;
|
|
security?:
|
|
| ChannelSecurityAdapter<TResolvedAccount>
|
|
| ChatChannelSecurityOptions<TResolvedAccount>;
|
|
pairing?: ChannelPairingAdapter | ChatChannelPairingOptions;
|
|
threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>;
|
|
outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions;
|
|
}): ChannelPlugin<TResolvedAccount, Probe, Audit> {
|
|
return {
|
|
...params.base,
|
|
conversationBindings: {
|
|
supportsCurrentConversationBinding: true,
|
|
...params.base.conversationBindings,
|
|
},
|
|
...(params.security ? { security: resolveChatChannelSecurity(params.security) } : {}),
|
|
...(params.pairing ? { pairing: resolveChatChannelPairing(params.pairing) } : {}),
|
|
...(params.threading ? { threading: resolveChatChannelThreading(params.threading) } : {}),
|
|
...(params.outbound ? { outbound: resolveChatChannelOutbound(params.outbound) } : {}),
|
|
} as ChannelPlugin<TResolvedAccount, Probe, Audit>;
|
|
}
|