refactor: finish decoupling plugin sdk seams

This commit is contained in:
Peter Steinberger
2026-03-29 22:41:47 +01:00
parent 574d3c5213
commit 2e0682d930
31 changed files with 499 additions and 501 deletions

View File

@@ -1,10 +1,10 @@
import type { OpenClawConfig } from "../config/config.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
import {
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./imessage-targets.js";
} from "./channel-targets.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
// Narrow plugin-sdk surface for the bundled BlueBubbles plugin.
// Keep this list additive and scoped to the conversation-binding seam only.
@@ -324,7 +324,7 @@ export {
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./imessage-targets.js";
} from "./channel-targets.js";
export { stripMarkdown } from "./text-runtime.js";
export { parseFiniteNumber } from "../infra/parse-finite-number.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";

View File

@@ -13,11 +13,10 @@ const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const ALLOWED_EXTENSION_PUBLIC_SURFACES = new Set(GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES);
ALLOWED_EXTENSION_PUBLIC_SURFACES.add("test-api.js");
const BUNDLED_EXTENSION_IDS = new Set(
readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "shared")
.map((entry) => entry.name),
);
const BUNDLED_EXTENSION_IDS = readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "shared")
.map((entry) => entry.name)
.toSorted((left, right) => right.length - left.length);
const GUARDED_CHANNEL_EXTENSIONS = new Set([
"bluebubbles",
"discord",
@@ -190,6 +189,7 @@ const LOCAL_EXTENSION_API_BARREL_GUARDS = [
"diffs",
"feishu",
"google",
"imessage",
"irc",
"llm-task",
"line",
@@ -472,7 +472,12 @@ function expectNoCrossPluginSdkFacadeImports(file: string, imports: string[]): v
continue;
}
const targetSubpath = specifier.slice("openclaw/plugin-sdk/".length);
if (!BUNDLED_EXTENSION_IDS.has(targetSubpath) || targetSubpath === currentExtensionId) {
const targetExtensionId =
BUNDLED_EXTENSION_IDS.find(
(extensionId) =>
targetSubpath === extensionId || targetSubpath.startsWith(`${extensionId}-`),
) ?? null;
if (!targetExtensionId || targetExtensionId === currentExtensionId) {
continue;
}
expect.fail(
@@ -585,7 +590,7 @@ describe("channel import guardrails", () => {
expect(
text,
`${normalized} should import ${extensionId} helpers via the local api barrel`,
).not.toMatch(new RegExp(`["']openclaw/plugin-sdk/${extensionId}["']`, "u"));
).not.toMatch(new RegExp(`["']openclaw/plugin-sdk/${extensionId}(?:["'/])`, "u"));
}
}
});

View File

@@ -23,6 +23,20 @@ export {
type MessagingTargetKind,
type MessagingTargetParseOptions,
} from "../channels/targets.js";
export {
createAllowedChatSenderMatcher,
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedChatTarget,
resolveServicePrefixedOrChatAllowTarget,
resolveServicePrefixedTarget,
type ChatSenderAllowParams,
type ChatTargetPrefixesParams,
type ParsedChatAllowTarget,
type ParsedChatTarget,
type ServicePrefix,
} from "../channels/plugins/chat-target-prefixes.js";
export {
buildUnresolvedTargetResults,
resolveTargetsWithOptionalToken,

View File

@@ -1,134 +0,0 @@
import {
normalizeIMessageHandle,
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./imessage-targets.js";
export type { ChannelPlugin } from "./channel-plugin-common.js";
export {
DEFAULT_ACCOUNT_ID,
buildChannelConfigSchema,
deleteAccountFromConfigSection,
getChatChannelMeta,
setAccountEnabledInConfigSection,
} from "./channel-plugin-common.js";
export {
formatTrimmedAllowFromEntries,
resolveIMessageConfigAllowFrom,
resolveIMessageConfigDefaultTo,
} from "./channel-config-helpers.js";
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
export {
normalizeIMessageHandle,
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./imessage-targets.js";
type IMessageService = "imessage" | "sms" | "auto";
type IMessageTarget = ParsedChatTarget | { kind: "handle"; to: string; service: IMessageService };
const CHAT_ID_PREFIXES = ["chat_id:", "chatid:", "chat:"];
const CHAT_GUID_PREFIXES = ["chat_guid:", "chatguid:", "guid:"];
const CHAT_IDENTIFIER_PREFIXES = ["chat_identifier:", "chatidentifier:", "chatident:"];
const SERVICE_PREFIXES: Array<{ prefix: string; service: IMessageService }> = [
{ prefix: "imessage:", service: "imessage" },
{ prefix: "sms:", service: "sms" },
{ prefix: "auto:", service: "auto" },
];
function startsWithAnyPrefix(value: string, prefixes: readonly string[]): boolean {
return prefixes.some((prefix) => value.startsWith(prefix));
}
function parseIMessageTarget(raw: string): IMessageTarget {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error("iMessage target is required");
}
const lower = trimmed.toLowerCase();
const servicePrefixed = resolveServicePrefixedTarget({
trimmed,
lower,
servicePrefixes: SERVICE_PREFIXES,
isChatTarget: (remainderLower) =>
startsWithAnyPrefix(remainderLower, [
...CHAT_ID_PREFIXES,
...CHAT_GUID_PREFIXES,
...CHAT_IDENTIFIER_PREFIXES,
]),
parseTarget: parseIMessageTarget,
});
if (servicePrefixed) {
return servicePrefixed;
}
const chatTarget = parseChatTargetPrefixesOrThrow({
trimmed,
lower,
chatIdPrefixes: CHAT_ID_PREFIXES,
chatGuidPrefixes: CHAT_GUID_PREFIXES,
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES,
});
if (chatTarget) {
return chatTarget;
}
return { kind: "handle", to: trimmed, service: "auto" };
}
export function normalizeIMessageAcpConversationId(
conversationId: string,
): { conversationId: string } | null {
const trimmed = conversationId.trim();
if (!trimmed) {
return null;
}
try {
const parsed = parseIMessageTarget(trimmed);
if (parsed.kind === "handle") {
const handle = normalizeIMessageHandle(parsed.to);
return handle ? { conversationId: handle } : null;
}
if (parsed.kind === "chat_id") {
return { conversationId: String(parsed.chatId) };
}
if (parsed.kind === "chat_guid") {
return { conversationId: parsed.chatGuid };
}
return { conversationId: parsed.chatIdentifier };
} catch {
const handle = normalizeIMessageHandle(trimmed);
return handle ? { conversationId: handle } : null;
}
}
export function matchIMessageAcpConversation(params: {
bindingConversationId: string;
conversationId: string;
}): { conversationId: string; matchPriority: number } | null {
const binding = normalizeIMessageAcpConversationId(params.bindingConversationId);
const conversation = normalizeIMessageAcpConversationId(params.conversationId);
if (!binding || !conversation) {
return null;
}
if (binding.conversationId !== conversation.conversationId) {
return null;
}
return {
conversationId: conversation.conversationId,
matchPriority: 2,
};
}
export function resolveIMessageConversationIdFromTarget(target: string): string | undefined {
return normalizeIMessageAcpConversationId(target)?.conversationId;
}

View File

@@ -1,40 +0,0 @@
// Generated by scripts/generate-plugin-sdk-facades.mjs. Do not edit manually.
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
type FacadeEntry = PluginSdkFacadeTypeMap["imessage-targets"];
type FacadeModule = FacadeEntry["module"];
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "imessage",
artifactBasename: "api.js",
});
}
export const normalizeIMessageHandle: FacadeModule["normalizeIMessageHandle"] = ((...args) =>
loadFacadeModule()["normalizeIMessageHandle"](
...args,
)) as FacadeModule["normalizeIMessageHandle"];
export const parseChatAllowTargetPrefixes: FacadeModule["parseChatAllowTargetPrefixes"] = ((
...args
) =>
loadFacadeModule()["parseChatAllowTargetPrefixes"](
...args,
)) as FacadeModule["parseChatAllowTargetPrefixes"];
export const parseChatTargetPrefixesOrThrow: FacadeModule["parseChatTargetPrefixesOrThrow"] = ((
...args
) =>
loadFacadeModule()["parseChatTargetPrefixesOrThrow"](
...args,
)) as FacadeModule["parseChatTargetPrefixesOrThrow"];
export const resolveServicePrefixedAllowTarget: FacadeModule["resolveServicePrefixedAllowTarget"] =
((...args) =>
loadFacadeModule()["resolveServicePrefixedAllowTarget"](
...args,
)) as FacadeModule["resolveServicePrefixedAllowTarget"];
export const resolveServicePrefixedTarget: FacadeModule["resolveServicePrefixedTarget"] = ((
...args
) =>
loadFacadeModule()["resolveServicePrefixedTarget"](
...args,
)) as FacadeModule["resolveServicePrefixedTarget"];
export type ParsedChatTarget = FacadeEntry["types"]["ParsedChatTarget"];

View File

@@ -35,12 +35,16 @@ export {
normalizeIMessageMessagingTarget,
} from "../channels/plugins/normalize/imessage.js";
export {
createAllowedChatSenderMatcher,
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedChatTarget,
resolveServicePrefixedOrChatAllowTarget,
resolveServicePrefixedTarget,
type ChatSenderAllowParams,
type ParsedChatTarget,
} from "./imessage-targets.js";
} from "./channel-targets.js";
export {
resolveAllowlistProviderRuntimeGroupPolicy,
@@ -70,6 +74,12 @@ type IMessageFacadeModule = {
accountId?: string;
cfg: OpenClawConfig;
}) => IMessageConversationBindingManager;
matchIMessageAcpConversation: (params: {
bindingConversationId: string;
conversationId: string;
}) => { conversationId: string; matchPriority: number } | null;
normalizeIMessageAcpConversationId: (conversationId: string) => { conversationId: string } | null;
resolveIMessageConversationIdFromTarget: (target: string) => string | undefined;
};
function loadIMessageFacadeModule(): IMessageFacadeModule {
@@ -85,3 +95,20 @@ export function createIMessageConversationBindingManager(params: {
}): IMessageConversationBindingManager {
return loadIMessageFacadeModule().createIMessageConversationBindingManager(params);
}
export function normalizeIMessageAcpConversationId(
conversationId: string,
): { conversationId: string } | null {
return loadIMessageFacadeModule().normalizeIMessageAcpConversationId(conversationId);
}
export function matchIMessageAcpConversation(params: {
bindingConversationId: string;
conversationId: string;
}): { conversationId: string; matchPriority: number } | null {
return loadIMessageFacadeModule().matchIMessageAcpConversation(params);
}
export function resolveIMessageConversationIdFromTarget(target: string): string | undefined {
return loadIMessageFacadeModule().resolveIMessageConversationIdFromTarget(target);
}

View File

@@ -199,22 +199,16 @@ describe("plugin-sdk subpath exports", () => {
expectSourceContains("telegram", 'export * from "./telegram-core.js";');
expectSourceContains("telegram", 'export * from "./telegram-runtime.js";');
expectSourceMentions("imessage", [
"normalizeIMessageAcpConversationId",
"matchIMessageAcpConversation",
"normalizeIMessageHandle",
"parseChatAllowTargetPrefixes",
"parseChatTargetPrefixesOrThrow",
"resolveIMessageConversationIdFromTarget",
"resolveServicePrefixedAllowTarget",
"resolveServicePrefixedTarget",
"chunkTextForOutbound",
]);
expectSourceMentions("imessage-core", [
"normalizeIMessageAcpConversationId",
"matchIMessageAcpConversation",
"resolveIMessageConversationIdFromTarget",
"parseChatAllowTargetPrefixes",
"parseChatTargetPrefixesOrThrow",
"resolveServicePrefixedAllowTarget",
"resolveServicePrefixedTarget",
]);
expectSourceMentions("bluebubbles", [
"normalizeBlueBubblesAcpConversationId",
"matchBlueBubblesAcpConversation",
@@ -503,11 +497,18 @@ describe("plugin-sdk subpath exports", () => {
"applyChannelMatchMeta",
"buildChannelKeyCandidates",
"buildMessagingTarget",
"createAllowedChatSenderMatcher",
"ensureTargetId",
"parseChatAllowTargetPrefixes",
"parseMentionPrefixOrAtUserTarget",
"parseChatTargetPrefixesOrThrow",
"requireTargetKind",
"resolveChannelEntryMatchWithFallback",
"resolveChannelMatchConfig",
"resolveServicePrefixedAllowTarget",
"resolveServicePrefixedChatTarget",
"resolveServicePrefixedOrChatAllowTarget",
"resolveServicePrefixedTarget",
"resolveTargetsWithOptionalToken",
]);
expectSourceMentions("channel-config-writes", [