mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 15:21:06 +00:00
refactor(plugins): narrow bundled channel core seams
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.core.js";
|
||||
import type { ChannelStatusIssue } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
type ParsedChatTarget,
|
||||
} from "./channel-targets.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { asString, collectIssuesForEnabledAccounts, isRecord } from "./status-helpers.js";
|
||||
|
||||
// Narrow plugin-sdk surface for the bundled BlueBubbles plugin.
|
||||
// Keep this list additive and scoped to the conversation-binding seam only.
|
||||
@@ -27,6 +25,7 @@ type BlueBubblesFacadeModule = {
|
||||
accountId?: string;
|
||||
cfg: OpenClawConfig;
|
||||
}) => BlueBubblesConversationBindingManager;
|
||||
collectBlueBubblesStatusIssues: (accounts: unknown[]) => ChannelStatusIssue[];
|
||||
};
|
||||
|
||||
function loadBlueBubblesFacadeModule(): BlueBubblesFacadeModule {
|
||||
@@ -266,99 +265,8 @@ export function resolveBlueBubblesConversationIdFromTarget(target: string): stri
|
||||
return normalizeBlueBubblesAcpConversationId(target)?.conversationId;
|
||||
}
|
||||
|
||||
type BlueBubblesAccountStatus = {
|
||||
accountId?: unknown;
|
||||
enabled?: unknown;
|
||||
configured?: unknown;
|
||||
running?: unknown;
|
||||
baseUrl?: unknown;
|
||||
lastError?: unknown;
|
||||
probe?: unknown;
|
||||
};
|
||||
|
||||
type BlueBubblesProbeResult = {
|
||||
ok?: boolean;
|
||||
status?: number | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
function readBlueBubblesAccountStatus(
|
||||
value: ChannelAccountSnapshot,
|
||||
): BlueBubblesAccountStatus | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
running: value.running,
|
||||
baseUrl: value.baseUrl,
|
||||
lastError: value.lastError,
|
||||
probe: value.probe,
|
||||
};
|
||||
}
|
||||
|
||||
function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: typeof value.ok === "boolean" ? value.ok : undefined,
|
||||
status: typeof value.status === "number" ? value.status : null,
|
||||
error: asString(value.error) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function collectBlueBubblesStatusIssues(
|
||||
accounts: ChannelAccountSnapshot[],
|
||||
): ChannelStatusIssue[] {
|
||||
return collectIssuesForEnabledAccounts({
|
||||
accounts,
|
||||
readAccount: readBlueBubblesAccountStatus,
|
||||
collectIssues: ({ account, accountId, issues }) => {
|
||||
const configured = account.configured === true;
|
||||
const running = account.running === true;
|
||||
const lastError = asString(account.lastError);
|
||||
const probe = readBlueBubblesProbeResult(account.probe);
|
||||
|
||||
if (!configured) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Not configured (missing serverUrl or password).",
|
||||
fix: "Run: openclaw channels add bluebubbles --http-url <server-url> --password <password>",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (probe && probe.ok === false) {
|
||||
const errorDetail = probe.error
|
||||
? `: ${probe.error}`
|
||||
: probe.status
|
||||
? ` (HTTP ${probe.status})`
|
||||
: "";
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `BlueBubbles server unreachable${errorDetail}`,
|
||||
fix: "Check that the BlueBubbles server is running and accessible. Verify serverUrl and password in your config.",
|
||||
});
|
||||
}
|
||||
|
||||
if (running && lastError) {
|
||||
issues.push({
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `Channel error: ${lastError}`,
|
||||
fix: "Check gateway logs for details. If the webhook is failing, verify the webhook URL is configured in BlueBubbles server settings.",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
export function collectBlueBubblesStatusIssues(accounts: unknown[]): ChannelStatusIssue[] {
|
||||
return loadBlueBubblesFacadeModule().collectBlueBubblesStatusIssues(accounts);
|
||||
}
|
||||
|
||||
export { resolveAckReaction } from "../agents/identity.js";
|
||||
|
||||
377
src/plugin-sdk/channel-core.ts
Normal file
377
src/plugin-sdk/channel-core.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
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>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getChatChannelMeta } from "../channels/chat-meta.js";
|
||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../channels/ids.js";
|
||||
import { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
createScopedAccountReplyToModeResolver,
|
||||
@@ -15,12 +15,15 @@ import type {
|
||||
ChannelPollResult,
|
||||
ChannelThreadingAdapter,
|
||||
} from "../channels/plugins/types.core.js";
|
||||
import type { ChannelMeta } from "../channels/plugins/types.js";
|
||||
import type { 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 { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
import type { PluginPackageChannel } from "../plugins/manifest.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
|
||||
|
||||
@@ -155,7 +158,6 @@ export {
|
||||
formatPairingApproveHint,
|
||||
parseOptionalDelimitedEntries,
|
||||
} from "../channels/plugins/helpers.js";
|
||||
export { getChatChannelMeta } from "../channels/chat-meta.js";
|
||||
export {
|
||||
channelTargetSchema,
|
||||
channelTargetsSchema,
|
||||
@@ -205,6 +207,105 @@ export type ChannelOutboundSessionRouteParams = Parameters<
|
||||
NonNullable<ChannelMessagingAdapter["resolveOutboundSessionRoute"]>
|
||||
>[0];
|
||||
|
||||
var cachedSdkChatChannelMeta: ReturnType<typeof buildChatChannelMetaById> | undefined;
|
||||
var cachedSdkChatChannelIdSet: Set<string> | undefined;
|
||||
|
||||
function getSdkChatChannelIdSet(): Set<string> {
|
||||
cachedSdkChatChannelIdSet ??= new Set(CHAT_CHANNEL_ORDER);
|
||||
return cachedSdkChatChannelIdSet;
|
||||
}
|
||||
|
||||
function toSdkChatChannelMeta(params: {
|
||||
id: ChatChannelId;
|
||||
channel: PluginPackageChannel;
|
||||
}): ChannelMeta {
|
||||
const label = params.channel.label?.trim();
|
||||
if (!label) {
|
||||
throw new Error(`Missing label for bundled chat channel "${params.id}"`);
|
||||
}
|
||||
return {
|
||||
id: params.id,
|
||||
label,
|
||||
selectionLabel: params.channel.selectionLabel?.trim() || label,
|
||||
docsPath: params.channel.docsPath?.trim() || `/channels/${params.id}`,
|
||||
docsLabel: params.channel.docsLabel?.trim() || undefined,
|
||||
blurb: params.channel.blurb?.trim() || "",
|
||||
...(params.channel.aliases?.length ? { aliases: params.channel.aliases } : {}),
|
||||
...(params.channel.order !== undefined ? { order: params.channel.order } : {}),
|
||||
...(params.channel.selectionDocsPrefix !== undefined
|
||||
? { selectionDocsPrefix: params.channel.selectionDocsPrefix }
|
||||
: {}),
|
||||
...(params.channel.selectionDocsOmitLabel !== undefined
|
||||
? { selectionDocsOmitLabel: params.channel.selectionDocsOmitLabel }
|
||||
: {}),
|
||||
...(params.channel.selectionExtras?.length
|
||||
? { selectionExtras: params.channel.selectionExtras }
|
||||
: {}),
|
||||
...(params.channel.detailLabel?.trim()
|
||||
? { detailLabel: params.channel.detailLabel.trim() }
|
||||
: {}),
|
||||
...(params.channel.systemImage?.trim()
|
||||
? { systemImage: params.channel.systemImage.trim() }
|
||||
: {}),
|
||||
...(params.channel.markdownCapable !== undefined
|
||||
? { markdownCapable: params.channel.markdownCapable }
|
||||
: {}),
|
||||
...(params.channel.showConfigured !== undefined
|
||||
? { showConfigured: params.channel.showConfigured }
|
||||
: {}),
|
||||
...(params.channel.quickstartAllowFrom !== undefined
|
||||
? { quickstartAllowFrom: params.channel.quickstartAllowFrom }
|
||||
: {}),
|
||||
...(params.channel.forceAccountBinding !== undefined
|
||||
? { forceAccountBinding: params.channel.forceAccountBinding }
|
||||
: {}),
|
||||
...(params.channel.preferSessionLookupForAnnounceTarget !== undefined
|
||||
? {
|
||||
preferSessionLookupForAnnounceTarget: params.channel.preferSessionLookupForAnnounceTarget,
|
||||
}
|
||||
: {}),
|
||||
...(params.channel.preferOver?.length ? { preferOver: params.channel.preferOver } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildChatChannelMetaById(): Record<ChatChannelId, ChannelMeta> {
|
||||
const entries = new Map<ChatChannelId, ChannelMeta>();
|
||||
for (const entry of listBundledPluginMetadata({
|
||||
includeChannelConfigs: true,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
})) {
|
||||
const channel =
|
||||
entry.packageManifest && "channel" in entry.packageManifest
|
||||
? entry.packageManifest.channel
|
||||
: undefined;
|
||||
if (!channel) {
|
||||
continue;
|
||||
}
|
||||
const rawId = channel.id?.trim();
|
||||
if (!rawId || !getSdkChatChannelIdSet().has(rawId)) {
|
||||
continue;
|
||||
}
|
||||
const id = rawId;
|
||||
entries.set(
|
||||
id,
|
||||
toSdkChatChannelMeta({
|
||||
id,
|
||||
channel,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Object.freeze(Object.fromEntries(entries)) as Record<ChatChannelId, ChannelMeta>;
|
||||
}
|
||||
|
||||
function resolveSdkChatChannelMeta(id: string) {
|
||||
cachedSdkChatChannelMeta ??= buildChatChannelMetaById();
|
||||
return cachedSdkChatChannelMeta[id];
|
||||
}
|
||||
|
||||
export function getChatChannelMeta(id: ChatChannelId): ChannelMeta {
|
||||
return resolveSdkChatChannelMeta(id);
|
||||
}
|
||||
|
||||
/** Remove one of the known provider prefixes from a free-form target string. */
|
||||
export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string {
|
||||
const trimmed = raw.trim();
|
||||
@@ -604,7 +705,7 @@ export function createChannelPluginBase<TResolvedAccount>(
|
||||
return {
|
||||
id: params.id,
|
||||
meta: {
|
||||
...getChatChannelMeta(params.id as Parameters<typeof getChatChannelMeta>[0]),
|
||||
...resolveSdkChatChannelMeta(params.id),
|
||||
...params.meta,
|
||||
},
|
||||
...(params.setupWizard ? { setupWizard: params.setupWizard } : {}),
|
||||
|
||||
Reference in New Issue
Block a user