diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index c1eb2642e22..3e85c051003 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -1004,8 +1004,10 @@ Compatibility note: helper is only needed by a bundled extension, keep it behind the extension's local `api.js` or `runtime-api.js` seam instead of promoting it into `openclaw/plugin-sdk/`. -- Channel-branded bundled bars stay private unless they are explicitly added - back to the public contract. +- New shared helper seams should be generic, not channel-branded. Shared target + parsing belongs on `openclaw/plugin-sdk/channel-targets`; channel-specific + internals stay behind the owning plugin's local `api.js` or `runtime-api.js` + seam. - Capability-specific subpaths such as `image-generation`, `media-understanding`, and `speech` exist because bundled/native plugins use them today. Their presence does not by itself mean every exported helper is a diff --git a/extensions/bluebubbles/src/conversation-route.test.ts b/extensions/bluebubbles/src/conversation-route.test.ts index a9946fcc4dd..d4c91e94526 100644 --- a/extensions/bluebubbles/src/conversation-route.test.ts +++ b/extensions/bluebubbles/src/conversation-route.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { setDefaultChannelPluginRegistryForTests } from "../../../src/commands/channel-test-helpers.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; import { __testing as sessionBindingTesting, @@ -17,7 +16,6 @@ const baseCfg = { describe("resolveBlueBubblesConversationRoute", () => { beforeEach(() => { sessionBindingTesting.resetSessionBindingAdaptersForTests(); - setDefaultChannelPluginRegistryForTests(); }); it("lets runtime BlueBubbles conversation bindings override default routing", () => { diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index 605c5cecc76..e8980088d3b 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -5,7 +5,7 @@ import { type ParsedChatTarget, resolveServicePrefixedAllowTarget, resolveServicePrefixedTarget, -} from "openclaw/plugin-sdk/imessage-core"; +} from "openclaw/plugin-sdk/channel-targets"; export type BlueBubblesService = "imessage" | "sms" | "auto"; diff --git a/extensions/imessage/src/conversation-id-core.ts b/extensions/imessage/src/conversation-id-core.ts new file mode 100644 index 00000000000..03b408d01b1 --- /dev/null +++ b/extensions/imessage/src/conversation-id-core.ts @@ -0,0 +1,50 @@ +import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js"; + +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; +} diff --git a/extensions/imessage/src/conversation-id.ts b/extensions/imessage/src/conversation-id.ts index 2088bd2dc2f..ccf620eb6f7 100644 --- a/extensions/imessage/src/conversation-id.ts +++ b/extensions/imessage/src/conversation-id.ts @@ -2,7 +2,7 @@ import { matchIMessageAcpConversation, normalizeIMessageAcpConversationId, resolveIMessageConversationIdFromTarget, -} from "openclaw/plugin-sdk/imessage-core"; +} from "./conversation-id-core.js"; import { normalizeIMessageHandle } from "./targets.js"; export { diff --git a/extensions/imessage/src/conversation-route.test.ts b/extensions/imessage/src/conversation-route.test.ts index 1cc2fa2a4dd..1f9c4c9394f 100644 --- a/extensions/imessage/src/conversation-route.test.ts +++ b/extensions/imessage/src/conversation-route.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { setDefaultChannelPluginRegistryForTests } from "../../../src/commands/channel-test-helpers.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; import { __testing as sessionBindingTesting, @@ -17,7 +16,6 @@ const baseCfg = { describe("resolveIMessageConversationRoute", () => { beforeEach(() => { sessionBindingTesting.resetSessionBindingAdaptersForTests(); - setDefaultChannelPluginRegistryForTests(); }); it("lets runtime iMessage conversation bindings override default routing", () => { diff --git a/extensions/imessage/src/target-parsing-helpers.ts b/extensions/imessage/src/target-parsing-helpers.ts index 04881fa2131..8d56e2f0bf4 100644 --- a/extensions/imessage/src/target-parsing-helpers.ts +++ b/extensions/imessage/src/target-parsing-helpers.ts @@ -1,223 +1,14 @@ -import { isAllowedParsedChatSender } from "openclaw/plugin-sdk/allow-from"; - -export type ServicePrefix = { prefix: string; service: TService }; - -export type ChatTargetPrefixesParams = { - trimmed: string; - lower: string; - chatIdPrefixes: string[]; - chatGuidPrefixes: string[]; - chatIdentifierPrefixes: string[]; -}; - -export type ParsedChatTarget = - | { kind: "chat_id"; chatId: number } - | { kind: "chat_guid"; chatGuid: string } - | { kind: "chat_identifier"; chatIdentifier: string }; - -export type ParsedChatAllowTarget = ParsedChatTarget | { kind: "handle"; handle: string }; - -export type ChatSenderAllowParams = { - allowFrom: Array; - sender: string; - chatId?: number | null; - chatGuid?: string | null; - chatIdentifier?: string | null; -}; - -function stripPrefix(value: string, prefix: string): string { - return value.slice(prefix.length).trim(); -} - -function startsWithAnyPrefix(value: string, prefixes: readonly string[]): boolean { - return prefixes.some((prefix) => value.startsWith(prefix)); -} - -export function resolveServicePrefixedTarget(params: { - trimmed: string; - lower: string; - servicePrefixes: Array>; - isChatTarget: (remainderLower: string) => boolean; - parseTarget: (remainder: string) => TTarget; -}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null { - for (const { prefix, service } of params.servicePrefixes) { - if (!params.lower.startsWith(prefix)) { - continue; - } - const remainder = stripPrefix(params.trimmed, prefix); - if (!remainder) { - throw new Error(`${prefix} target is required`); - } - const remainderLower = remainder.toLowerCase(); - if (params.isChatTarget(remainderLower)) { - return params.parseTarget(remainder); - } - return { kind: "handle", to: remainder, service }; - } - return null; -} - -export function resolveServicePrefixedChatTarget(params: { - trimmed: string; - lower: string; - servicePrefixes: Array>; - chatIdPrefixes: string[]; - chatGuidPrefixes: string[]; - chatIdentifierPrefixes: string[]; - extraChatPrefixes?: string[]; - parseTarget: (remainder: string) => TTarget; -}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null { - const chatPrefixes = [ - ...params.chatIdPrefixes, - ...params.chatGuidPrefixes, - ...params.chatIdentifierPrefixes, - ...(params.extraChatPrefixes ?? []), - ]; - return resolveServicePrefixedTarget({ - trimmed: params.trimmed, - lower: params.lower, - servicePrefixes: params.servicePrefixes, - isChatTarget: (remainderLower) => startsWithAnyPrefix(remainderLower, chatPrefixes), - parseTarget: params.parseTarget, - }); -} - -export function parseChatTargetPrefixesOrThrow( - params: ChatTargetPrefixesParams, -): ParsedChatTarget | null { - for (const prefix of params.chatIdPrefixes) { - if (params.lower.startsWith(prefix)) { - const value = stripPrefix(params.trimmed, prefix); - const chatId = Number.parseInt(value, 10); - if (!Number.isFinite(chatId)) { - throw new Error(`Invalid chat_id: ${value}`); - } - return { kind: "chat_id", chatId }; - } - } - - for (const prefix of params.chatGuidPrefixes) { - if (params.lower.startsWith(prefix)) { - const value = stripPrefix(params.trimmed, prefix); - if (!value) { - throw new Error("chat_guid is required"); - } - return { kind: "chat_guid", chatGuid: value }; - } - } - - for (const prefix of params.chatIdentifierPrefixes) { - if (params.lower.startsWith(prefix)) { - const value = stripPrefix(params.trimmed, prefix); - if (!value) { - throw new Error("chat_identifier is required"); - } - return { kind: "chat_identifier", chatIdentifier: value }; - } - } - - return null; -} - -export function resolveServicePrefixedAllowTarget(params: { - trimmed: string; - lower: string; - servicePrefixes: Array<{ prefix: string }>; - parseAllowTarget: (remainder: string) => TAllowTarget; -}): (TAllowTarget | { kind: "handle"; handle: string }) | null { - for (const { prefix } of params.servicePrefixes) { - if (!params.lower.startsWith(prefix)) { - continue; - } - const remainder = stripPrefix(params.trimmed, prefix); - if (!remainder) { - return { kind: "handle", handle: "" }; - } - return params.parseAllowTarget(remainder); - } - return null; -} - -export function resolveServicePrefixedOrChatAllowTarget< - TAllowTarget extends ParsedChatAllowTarget, ->(params: { - trimmed: string; - lower: string; - servicePrefixes: Array<{ prefix: string }>; - parseAllowTarget: (remainder: string) => TAllowTarget; - chatIdPrefixes: string[]; - chatGuidPrefixes: string[]; - chatIdentifierPrefixes: string[]; -}): TAllowTarget | null { - const servicePrefixed = resolveServicePrefixedAllowTarget({ - trimmed: params.trimmed, - lower: params.lower, - servicePrefixes: params.servicePrefixes, - parseAllowTarget: params.parseAllowTarget, - }); - if (servicePrefixed) { - return servicePrefixed as TAllowTarget; - } - - const chatTarget = parseChatAllowTargetPrefixes({ - trimmed: params.trimmed, - lower: params.lower, - chatIdPrefixes: params.chatIdPrefixes, - chatGuidPrefixes: params.chatGuidPrefixes, - chatIdentifierPrefixes: params.chatIdentifierPrefixes, - }); - if (chatTarget) { - return chatTarget as TAllowTarget; - } - return null; -} - -export function createAllowedChatSenderMatcher(params: { - normalizeSender: (sender: string) => string; - parseAllowTarget: (entry: string) => TParsed; -}): (input: ChatSenderAllowParams) => boolean { - return (input) => - isAllowedParsedChatSender({ - allowFrom: input.allowFrom, - sender: input.sender, - chatId: input.chatId, - chatGuid: input.chatGuid, - chatIdentifier: input.chatIdentifier, - normalizeSender: params.normalizeSender, - parseAllowTarget: params.parseAllowTarget, - }); -} - -export function parseChatAllowTargetPrefixes( - params: ChatTargetPrefixesParams, -): ParsedChatTarget | null { - for (const prefix of params.chatIdPrefixes) { - if (params.lower.startsWith(prefix)) { - const value = stripPrefix(params.trimmed, prefix); - const chatId = Number.parseInt(value, 10); - if (Number.isFinite(chatId)) { - return { kind: "chat_id", chatId }; - } - } - } - - for (const prefix of params.chatGuidPrefixes) { - if (params.lower.startsWith(prefix)) { - const value = stripPrefix(params.trimmed, prefix); - if (value) { - return { kind: "chat_guid", chatGuid: value }; - } - } - } - - for (const prefix of params.chatIdentifierPrefixes) { - if (params.lower.startsWith(prefix)) { - const value = stripPrefix(params.trimmed, prefix); - if (value) { - return { kind: "chat_identifier", chatIdentifier: value }; - } - } - } - - return null; -} +export { + createAllowedChatSenderMatcher, + parseChatAllowTargetPrefixes, + parseChatTargetPrefixesOrThrow, + resolveServicePrefixedAllowTarget, + resolveServicePrefixedChatTarget, + resolveServicePrefixedOrChatAllowTarget, + resolveServicePrefixedTarget, + type ChatSenderAllowParams, + type ChatTargetPrefixesParams, + type ParsedChatAllowTarget, + type ParsedChatTarget, + type ServicePrefix, +} from "openclaw/plugin-sdk/channel-targets"; diff --git a/extensions/imessage/src/targets.ts b/extensions/imessage/src/targets.ts index f20d0c99eb0..8730c94b297 100644 --- a/extensions/imessage/src/targets.ts +++ b/extensions/imessage/src/targets.ts @@ -1,4 +1,4 @@ -import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; +import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution"; import { createAllowedChatSenderMatcher, type ChatSenderAllowParams, diff --git a/package.json b/package.json index 7746de62da2..a0fddd9a8fd 100644 --- a/package.json +++ b/package.json @@ -494,10 +494,6 @@ "types": "./dist/plugin-sdk/imessage.d.ts", "default": "./dist/plugin-sdk/imessage.js" }, - "./plugin-sdk/imessage-core": { - "types": "./dist/plugin-sdk/imessage-core.d.ts", - "default": "./dist/plugin-sdk/imessage-core.js" - }, "./plugin-sdk/imessage-policy": { "types": "./dist/plugin-sdk/imessage-policy.d.ts", "default": "./dist/plugin-sdk/imessage-policy.js" @@ -506,10 +502,6 @@ "types": "./dist/plugin-sdk/imessage-runtime.d.ts", "default": "./dist/plugin-sdk/imessage-runtime.js" }, - "./plugin-sdk/imessage-targets": { - "types": "./dist/plugin-sdk/imessage-targets.d.ts", - "default": "./dist/plugin-sdk/imessage-targets.js" - }, "./plugin-sdk/irc": { "types": "./dist/plugin-sdk/irc.d.ts", "default": "./dist/plugin-sdk/irc.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 8eb48340f2f..80e2436baba 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -113,10 +113,8 @@ "image-generation", "image-generation-core", "imessage", - "imessage-core", "imessage-policy", "imessage-runtime", - "imessage-targets", "irc", "irc-surface", "kimi-coding", diff --git a/scripts/lib/plugin-sdk-facades.mjs b/scripts/lib/plugin-sdk-facades.mjs index ad482d2a6ba..15eff62b0df 100644 --- a/scripts/lib/plugin-sdk-facades.mjs +++ b/scripts/lib/plugin-sdk-facades.mjs @@ -427,19 +427,6 @@ export const GENERATED_PLUGIN_SDK_FACADES = [ "isHuggingfacePolicyLocked", ], }, - { - subpath: "imessage-targets", - source: pluginSource("imessage", "api.js"), - exports: [ - "normalizeIMessageHandle", - "parseChatAllowTargetPrefixes", - "parseChatTargetPrefixesOrThrow", - "resolveServicePrefixedAllowTarget", - "resolveServicePrefixedTarget", - "ParsedChatTarget", - ], - typeExports: ["ParsedChatTarget"], - }, { subpath: "image-generation-runtime", source: pluginSource("image-generation-core", "runtime-api.js"), diff --git a/src/channels/chat-meta.ts b/src/channels/chat-meta.ts index 70a22fea1d4..0bb7a4ad506 100644 --- a/src/channels/chat-meta.ts +++ b/src/channels/chat-meta.ts @@ -64,7 +64,10 @@ function toChatChannelMeta(params: { function buildChatChannelMetaById(): Record { const entries = new Map(); - for (const entry of listBundledPluginMetadata()) { + for (const entry of listBundledPluginMetadata({ + includeChannelConfigs: true, + includeSyntheticChannelConfigs: false, + })) { const channel = entry.packageManifest && "channel" in entry.packageManifest ? entry.packageManifest.channel diff --git a/src/channels/plugins/binding-targets.ts b/src/channels/plugins/binding-targets.ts index 2ca8fefea22..13c9d6c6d7f 100644 --- a/src/channels/plugins/binding-targets.ts +++ b/src/channels/plugins/binding-targets.ts @@ -10,7 +10,7 @@ export async function ensureConfiguredBindingTargetReady(params: { cfg: OpenClawConfig; bindingResolution: ConfiguredBindingResolution | null; }): Promise<{ ok: true } | { ok: false; error: string }> { - ensureStatefulTargetBuiltinsRegistered(); + await ensureStatefulTargetBuiltinsRegistered(); if (!params.bindingResolution) { return { ok: true }; } @@ -32,7 +32,7 @@ export async function resetConfiguredBindingTargetInPlace(params: { sessionKey: string; reason: "new" | "reset"; }): Promise<{ ok: true } | { ok: false; skipped?: boolean; error?: string }> { - ensureStatefulTargetBuiltinsRegistered(); + await ensureStatefulTargetBuiltinsRegistered(); const resolved = resolveStatefulBindingTargetBySessionKey({ cfg: params.cfg, sessionKey: params.sessionKey, @@ -53,7 +53,7 @@ export async function ensureConfiguredBindingTargetSession(params: { cfg: OpenClawConfig; bindingResolution: ConfiguredBindingResolution; }): Promise<{ ok: true; sessionKey: string } | { ok: false; sessionKey: string; error: string }> { - ensureStatefulTargetBuiltinsRegistered(); + await ensureStatefulTargetBuiltinsRegistered(); const driver = getStatefulBindingTargetDriver(params.bindingResolution.statefulTarget.driverId); if (!driver) { return { diff --git a/src/channels/plugins/chat-target-prefixes.ts b/src/channels/plugins/chat-target-prefixes.ts new file mode 100644 index 00000000000..7cb728f2ea8 --- /dev/null +++ b/src/channels/plugins/chat-target-prefixes.ts @@ -0,0 +1,269 @@ +export type ServicePrefix = { prefix: string; service: TService }; + +export type ChatTargetPrefixesParams = { + trimmed: string; + lower: string; + chatIdPrefixes: string[]; + chatGuidPrefixes: string[]; + chatIdentifierPrefixes: string[]; +}; + +export type ParsedChatTarget = + | { kind: "chat_id"; chatId: number } + | { kind: "chat_guid"; chatGuid: string } + | { kind: "chat_identifier"; chatIdentifier: string }; + +export type ParsedChatAllowTarget = ParsedChatTarget | { kind: "handle"; handle: string }; + +export type ChatSenderAllowParams = { + allowFrom: Array; + sender: string; + chatId?: number | null; + chatGuid?: string | null; + chatIdentifier?: string | null; +}; + +function isAllowedParsedChatSender(params: { + allowFrom: Array; + sender: string; + chatId?: number | null; + chatGuid?: string | null; + chatIdentifier?: string | null; + normalizeSender: (sender: string) => string; + parseAllowTarget: (entry: string) => TParsed; +}): boolean { + const allowFrom = params.allowFrom.map((entry) => String(entry).trim()); + if (allowFrom.length === 0) { + return false; + } + if (allowFrom.includes("*")) { + return true; + } + + const senderNormalized = params.normalizeSender(params.sender); + const chatId = params.chatId ?? undefined; + const chatGuid = params.chatGuid?.trim(); + const chatIdentifier = params.chatIdentifier?.trim(); + + for (const entry of allowFrom) { + if (!entry) { + continue; + } + const parsed = params.parseAllowTarget(entry); + if (parsed.kind === "chat_id" && chatId !== undefined) { + if (parsed.chatId === chatId) { + return true; + } + } else if (parsed.kind === "chat_guid" && chatGuid) { + if (parsed.chatGuid === chatGuid) { + return true; + } + } else if (parsed.kind === "chat_identifier" && chatIdentifier) { + if (parsed.chatIdentifier === chatIdentifier) { + return true; + } + } else if (parsed.kind === "handle" && senderNormalized) { + if (parsed.handle === senderNormalized) { + return true; + } + } + } + return false; +} + +function stripPrefix(value: string, prefix: string): string { + return value.slice(prefix.length).trim(); +} + +function startsWithAnyPrefix(value: string, prefixes: readonly string[]): boolean { + return prefixes.some((prefix) => value.startsWith(prefix)); +} + +export function resolveServicePrefixedTarget(params: { + trimmed: string; + lower: string; + servicePrefixes: Array>; + isChatTarget: (remainderLower: string) => boolean; + parseTarget: (remainder: string) => TTarget; +}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null { + for (const { prefix, service } of params.servicePrefixes) { + if (!params.lower.startsWith(prefix)) { + continue; + } + const remainder = stripPrefix(params.trimmed, prefix); + if (!remainder) { + throw new Error(`${prefix} target is required`); + } + const remainderLower = remainder.toLowerCase(); + if (params.isChatTarget(remainderLower)) { + return params.parseTarget(remainder); + } + return { kind: "handle", to: remainder, service }; + } + return null; +} + +export function resolveServicePrefixedChatTarget(params: { + trimmed: string; + lower: string; + servicePrefixes: Array>; + chatIdPrefixes: string[]; + chatGuidPrefixes: string[]; + chatIdentifierPrefixes: string[]; + extraChatPrefixes?: string[]; + parseTarget: (remainder: string) => TTarget; +}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null { + const chatPrefixes = [ + ...params.chatIdPrefixes, + ...params.chatGuidPrefixes, + ...params.chatIdentifierPrefixes, + ...(params.extraChatPrefixes ?? []), + ]; + return resolveServicePrefixedTarget({ + trimmed: params.trimmed, + lower: params.lower, + servicePrefixes: params.servicePrefixes, + isChatTarget: (remainderLower) => startsWithAnyPrefix(remainderLower, chatPrefixes), + parseTarget: params.parseTarget, + }); +} + +export function parseChatTargetPrefixesOrThrow( + params: ChatTargetPrefixesParams, +): ParsedChatTarget | null { + for (const prefix of params.chatIdPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + const chatId = Number.parseInt(value, 10); + if (!Number.isFinite(chatId)) { + throw new Error(`Invalid chat_id: ${value}`); + } + return { kind: "chat_id", chatId }; + } + } + + for (const prefix of params.chatGuidPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (!value) { + throw new Error("chat_guid is required"); + } + return { kind: "chat_guid", chatGuid: value }; + } + } + + for (const prefix of params.chatIdentifierPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (!value) { + throw new Error("chat_identifier is required"); + } + return { kind: "chat_identifier", chatIdentifier: value }; + } + } + + return null; +} + +export function resolveServicePrefixedAllowTarget(params: { + trimmed: string; + lower: string; + servicePrefixes: Array<{ prefix: string }>; + parseAllowTarget: (remainder: string) => TAllowTarget; +}): (TAllowTarget | { kind: "handle"; handle: string }) | null { + for (const { prefix } of params.servicePrefixes) { + if (!params.lower.startsWith(prefix)) { + continue; + } + const remainder = stripPrefix(params.trimmed, prefix); + if (!remainder) { + return { kind: "handle", handle: "" }; + } + return params.parseAllowTarget(remainder); + } + return null; +} + +export function resolveServicePrefixedOrChatAllowTarget< + TAllowTarget extends ParsedChatAllowTarget, +>(params: { + trimmed: string; + lower: string; + servicePrefixes: Array<{ prefix: string }>; + parseAllowTarget: (remainder: string) => TAllowTarget; + chatIdPrefixes: string[]; + chatGuidPrefixes: string[]; + chatIdentifierPrefixes: string[]; +}): TAllowTarget | null { + const servicePrefixed = resolveServicePrefixedAllowTarget({ + trimmed: params.trimmed, + lower: params.lower, + servicePrefixes: params.servicePrefixes, + parseAllowTarget: params.parseAllowTarget, + }); + if (servicePrefixed) { + return servicePrefixed as TAllowTarget; + } + + const chatTarget = parseChatAllowTargetPrefixes({ + trimmed: params.trimmed, + lower: params.lower, + chatIdPrefixes: params.chatIdPrefixes, + chatGuidPrefixes: params.chatGuidPrefixes, + chatIdentifierPrefixes: params.chatIdentifierPrefixes, + }); + if (chatTarget) { + return chatTarget as TAllowTarget; + } + return null; +} + +export function createAllowedChatSenderMatcher(params: { + normalizeSender: (sender: string) => string; + parseAllowTarget: (entry: string) => TParsed; +}): (input: ChatSenderAllowParams) => boolean { + return (input) => + isAllowedParsedChatSender({ + allowFrom: input.allowFrom, + sender: input.sender, + chatId: input.chatId, + chatGuid: input.chatGuid, + chatIdentifier: input.chatIdentifier, + normalizeSender: params.normalizeSender, + parseAllowTarget: params.parseAllowTarget, + }); +} + +export function parseChatAllowTargetPrefixes( + params: ChatTargetPrefixesParams, +): ParsedChatTarget | null { + for (const prefix of params.chatIdPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + const chatId = Number.parseInt(value, 10); + if (Number.isFinite(chatId)) { + return { kind: "chat_id", chatId }; + } + } + } + + for (const prefix of params.chatGuidPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (value) { + return { kind: "chat_guid", chatGuid: value }; + } + } + } + + for (const prefix of params.chatIdentifierPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (value) { + return { kind: "chat_identifier", chatIdentifier: value }; + } + } + } + + return null; +} diff --git a/src/channels/plugins/normalize/imessage.ts b/src/channels/plugins/normalize/imessage.ts index a6043632121..2884a568c66 100644 --- a/src/channels/plugins/normalize/imessage.ts +++ b/src/channels/plugins/normalize/imessage.ts @@ -1,4 +1,4 @@ -import { normalizeIMessageHandle } from "../../../plugin-sdk/imessage-targets.js"; +import { normalizeIMessageHandle } from "../../../plugin-sdk/imessage-policy.js"; import { looksLikeHandleOrPhoneTarget, trimMessagingTarget } from "./shared.js"; // Service prefixes that indicate explicit delivery method; must be preserved during normalization diff --git a/src/channels/plugins/stateful-target-builtins.ts b/src/channels/plugins/stateful-target-builtins.ts index 0d87ca31d2d..31b8144498e 100644 --- a/src/channels/plugins/stateful-target-builtins.ts +++ b/src/channels/plugins/stateful-target-builtins.ts @@ -1,13 +1,29 @@ -import { acpStatefulBindingTargetDriver } from "./acp-stateful-target-driver.js"; import { registerStatefulBindingTargetDriver, unregisterStatefulBindingTargetDriver, } from "./stateful-target-drivers.js"; -export function ensureStatefulTargetBuiltinsRegistered(): void { - registerStatefulBindingTargetDriver(acpStatefulBindingTargetDriver); +let builtinsRegisteredPromise: Promise | null = null; + +export async function ensureStatefulTargetBuiltinsRegistered(): Promise { + if (builtinsRegisteredPromise) { + await builtinsRegisteredPromise; + return; + } + builtinsRegisteredPromise = (async () => { + const { acpStatefulBindingTargetDriver } = await import("./acp-stateful-target-driver.js"); + registerStatefulBindingTargetDriver(acpStatefulBindingTargetDriver); + })(); + try { + await builtinsRegisteredPromise; + } catch (error) { + builtinsRegisteredPromise = null; + throw error; + } } -export function resetStatefulTargetBuiltinsForTesting(): void { +export async function resetStatefulTargetBuiltinsForTesting(): Promise { + builtinsRegisteredPromise = null; + const { acpStatefulBindingTargetDriver } = await import("./acp-stateful-target-driver.js"); unregisterStatefulBindingTargetDriver(acpStatefulBindingTargetDriver.id); } diff --git a/src/channels/session-meta.ts b/src/channels/session-meta.ts index 29b2d77e046..c8d5f6285af 100644 --- a/src/channels/session-meta.ts +++ b/src/channels/session-meta.ts @@ -1,6 +1,14 @@ import type { MsgContext } from "../auto-reply/templating.js"; import type { OpenClawConfig } from "../config/config.js"; -import { recordSessionMetaFromInbound, resolveStorePath } from "../config/sessions.js"; + +let inboundSessionRuntimePromise: Promise< + typeof import("../config/sessions/inbound.runtime.js") +> | null = null; + +function loadInboundSessionRuntime() { + inboundSessionRuntimePromise ??= import("../config/sessions/inbound.runtime.js"); + return inboundSessionRuntimePromise; +} export async function recordInboundSessionMetaSafe(params: { cfg: OpenClawConfig; @@ -9,11 +17,12 @@ export async function recordInboundSessionMetaSafe(params: { ctx: MsgContext; onError?: (error: unknown) => void; }): Promise { - const storePath = resolveStorePath(params.cfg.session?.store, { + const runtime = await loadInboundSessionRuntime(); + const storePath = runtime.resolveStorePath(params.cfg.session?.store, { agentId: params.agentId, }); try { - await recordSessionMetaFromInbound({ + await runtime.recordSessionMetaFromInbound({ storePath, sessionKey: params.sessionKey, ctx: params.ctx, diff --git a/src/channels/session.test.ts b/src/channels/session.test.ts index 13323a18318..143e0fa5564 100644 --- a/src/channels/session.test.ts +++ b/src/channels/session.test.ts @@ -4,7 +4,7 @@ import type { MsgContext } from "../auto-reply/templating.js"; const recordSessionMetaFromInboundMock = vi.fn((_args?: unknown) => Promise.resolve(undefined)); const updateLastRouteMock = vi.fn((_args?: unknown) => Promise.resolve(undefined)); -vi.mock("../config/sessions.js", () => ({ +vi.mock("../config/sessions/inbound.runtime.js", () => ({ recordSessionMetaFromInbound: (args: unknown) => recordSessionMetaFromInboundMock(args), updateLastRoute: (args: unknown) => updateLastRouteMock(args), })); diff --git a/src/channels/session.ts b/src/channels/session.ts index f71ef024a5f..22e8d43bcdc 100644 --- a/src/channels/session.ts +++ b/src/channels/session.ts @@ -1,10 +1,14 @@ import type { MsgContext } from "../auto-reply/templating.js"; -import { - recordSessionMetaFromInbound, - type GroupKeyResolution, - type SessionEntry, - updateLastRoute, -} from "../config/sessions.js"; +import type { GroupKeyResolution, SessionEntry } from "../config/sessions/types.js"; + +let inboundSessionRuntimePromise: Promise< + typeof import("../config/sessions/inbound.runtime.js") +> | null = null; + +function loadInboundSessionRuntime() { + inboundSessionRuntimePromise ??= import("../config/sessions/inbound.runtime.js"); + return inboundSessionRuntimePromise; +} function normalizeSessionStoreKey(sessionKey: string): string { return sessionKey.trim().toLowerCase(); @@ -49,13 +53,16 @@ export async function recordInboundSession(params: { }): Promise { const { storePath, sessionKey, ctx, groupResolution, createIfMissing } = params; const canonicalSessionKey = normalizeSessionStoreKey(sessionKey); - void recordSessionMetaFromInbound({ - storePath, - sessionKey: canonicalSessionKey, - ctx, - groupResolution, - createIfMissing, - }).catch(params.onRecordError); + const runtime = await loadInboundSessionRuntime(); + void runtime + .recordSessionMetaFromInbound({ + storePath, + sessionKey: canonicalSessionKey, + ctx, + groupResolution, + createIfMissing, + }) + .catch(params.onRecordError); const update = params.updateLastRoute; if (!update) { @@ -65,7 +72,7 @@ export async function recordInboundSession(params: { return; } const targetSessionKey = normalizeSessionStoreKey(update.sessionKey); - await updateLastRoute({ + await runtime.updateLastRoute({ storePath, sessionKey: targetSessionKey, deliveryContext: { diff --git a/src/config/sessions/inbound.runtime.ts b/src/config/sessions/inbound.runtime.ts new file mode 100644 index 00000000000..d7bfbb9d6f0 --- /dev/null +++ b/src/config/sessions/inbound.runtime.ts @@ -0,0 +1,2 @@ +export { resolveStorePath } from "./paths.js"; +export { recordSessionMetaFromInbound, updateLastRoute } from "./store.js"; diff --git a/src/generated/plugin-sdk-facade-type-map.generated.ts b/src/generated/plugin-sdk-facade-type-map.generated.ts index b1592034a13..3250a96408f 100644 --- a/src/generated/plugin-sdk-facade-type-map.generated.ts +++ b/src/generated/plugin-sdk-facade-type-map.generated.ts @@ -223,17 +223,6 @@ export interface PluginSdkFacadeTypeMap { }; types: {}; }; - "imessage-targets": { - module: typeof import("@openclaw/imessage/api.js"); - sourceModules: { - source1: { - module: typeof import("@openclaw/imessage/api.js"); - }; - }; - types: { - ParsedChatTarget: import("@openclaw/imessage/api.js").ParsedChatTarget; - }; - }; "image-generation-runtime": { module: typeof import("@openclaw/image-generation-core/runtime-api.js"); sourceModules: { diff --git a/src/infra/outbound/current-conversation-bindings.ts b/src/infra/outbound/current-conversation-bindings.ts index d636651f720..0f6817d6e9f 100644 --- a/src/infra/outbound/current-conversation-bindings.ts +++ b/src/infra/outbound/current-conversation-bindings.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import path from "node:path"; import { normalizeConversationText } from "../../acp/conversation-id.js"; -import { listBundledChannelPlugins } from "../../channels/plugins/bundled.js"; import { normalizeAnyChannelId } from "../../channels/registry.js"; import { resolveStateDir } from "../../config/paths.js"; import { loadJsonFile } from "../../infra/json-file.js"; @@ -128,9 +127,11 @@ function resolveChannelSupportsCurrentConversationBinding(channel: string): bool const matchesPluginId = (plugin: { id: string; meta?: { aliases?: readonly string[] } }) => plugin.id === normalized || (plugin.meta?.aliases ?? []).some((alias) => alias.trim().toLowerCase() === normalized); - const plugin = - getActivePluginChannelRegistry()?.channels.find((entry) => matchesPluginId(entry.plugin)) - ?.plugin ?? listBundledChannelPlugins().find((entry) => matchesPluginId(entry)); + // Keep this resolver on the active runtime registry only. Importing bundled + // channel loaders here creates a module cycle through plugin-sdk surfaces. + const plugin = getActivePluginChannelRegistry()?.channels.find((entry) => + matchesPluginId(entry.plugin), + )?.plugin; return plugin?.conversationBindings?.supportsCurrentConversationBinding === true; } diff --git a/src/plugin-sdk/bluebubbles.ts b/src/plugin-sdk/bluebubbles.ts index abe6ff80bef..8cf6cebbd34 100644 --- a/src/plugin-sdk/bluebubbles.ts +++ b/src/plugin-sdk/bluebubbles.ts @@ -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"; diff --git a/src/plugin-sdk/channel-import-guardrails.test.ts b/src/plugin-sdk/channel-import-guardrails.test.ts index c6c9d4e0289..bf6f4c2287f 100644 --- a/src/plugin-sdk/channel-import-guardrails.test.ts +++ b/src/plugin-sdk/channel-import-guardrails.test.ts @@ -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")); } } }); diff --git a/src/plugin-sdk/channel-targets.ts b/src/plugin-sdk/channel-targets.ts index c24bc9b3046..cbdef5d20a8 100644 --- a/src/plugin-sdk/channel-targets.ts +++ b/src/plugin-sdk/channel-targets.ts @@ -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, diff --git a/src/plugin-sdk/imessage-core.ts b/src/plugin-sdk/imessage-core.ts deleted file mode 100644 index 3e2ecb69cf8..00000000000 --- a/src/plugin-sdk/imessage-core.ts +++ /dev/null @@ -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; -} diff --git a/src/plugin-sdk/imessage-targets.ts b/src/plugin-sdk/imessage-targets.ts deleted file mode 100644 index 9d5bc5f2eff..00000000000 --- a/src/plugin-sdk/imessage-targets.ts +++ /dev/null @@ -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({ - 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"]; diff --git a/src/plugin-sdk/imessage.ts b/src/plugin-sdk/imessage.ts index 7732e8b9bc3..0c99f37bb37 100644 --- a/src/plugin-sdk/imessage.ts +++ b/src/plugin-sdk/imessage.ts @@ -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); +} diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 772e53123bc..bd4cc8d0bbc 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -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", [ diff --git a/src/plugins/bundled-plugin-metadata.ts b/src/plugins/bundled-plugin-metadata.ts index f169717462f..07894aeceda 100644 --- a/src/plugins/bundled-plugin-metadata.ts +++ b/src/plugins/bundled-plugin-metadata.ts @@ -360,6 +360,7 @@ function collectBundledChannelConfigs(params: { function collectBundledPluginMetadataForPackageRoot( packageRoot: string, includeChannelConfigs: boolean, + includeSyntheticChannelConfigs: boolean, ): readonly BundledPluginMetadata[] { const scanDir = resolveBundledPluginScanDir(packageRoot); if (!scanDir || !fs.existsSync(scanDir)) { @@ -404,13 +405,14 @@ function collectBundledPluginMetadataForPackageRoot( ...(setupSourcePath ? { setupEntry: setupSourcePath } : {}), }); const runtimeSidecarArtifacts = collectRuntimeSidecarArtifacts(publicSurfaceArtifacts); - const channelConfigs = includeChannelConfigs - ? collectBundledChannelConfigs({ - pluginDir, - manifest: manifestResult.manifest, - packageManifest, - }) - : manifestResult.manifest.channelConfigs; + const channelConfigs = + includeChannelConfigs && includeSyntheticChannelConfigs + ? collectBundledChannelConfigs({ + pluginDir, + manifest: manifestResult.manifest, + packageManifest, + }) + : manifestResult.manifest.channelConfigs; entries.push({ dirName, @@ -448,16 +450,27 @@ function collectBundledPluginMetadataForPackageRoot( export function listBundledPluginMetadata(params?: { rootDir?: string; includeChannelConfigs?: boolean; + includeSyntheticChannelConfigs?: boolean; }): readonly BundledPluginMetadata[] { const rootDir = path.resolve(params?.rootDir ?? OPENCLAW_PACKAGE_ROOT); const includeChannelConfigs = params?.includeChannelConfigs ?? !RUNNING_FROM_BUILT_ARTIFACT; - const cacheKey = JSON.stringify({ rootDir, includeChannelConfigs }); + const includeSyntheticChannelConfigs = + params?.includeSyntheticChannelConfigs ?? includeChannelConfigs; + const cacheKey = JSON.stringify({ + rootDir, + includeChannelConfigs, + includeSyntheticChannelConfigs, + }); const cached = bundledPluginMetadataCache.get(cacheKey); if (cached) { return cached; } const entries = Object.freeze( - collectBundledPluginMetadataForPackageRoot(rootDir, includeChannelConfigs), + collectBundledPluginMetadataForPackageRoot( + rootDir, + includeChannelConfigs, + includeSyntheticChannelConfigs, + ), ); bundledPluginMetadataCache.set(cacheKey, entries); return entries; diff --git a/src/test-utils/imessage-test-plugin.ts b/src/test-utils/imessage-test-plugin.ts index 083ffe3ad6f..bc72820b7e1 100644 --- a/src/test-utils/imessage-test-plugin.ts +++ b/src/test-utils/imessage-test-plugin.ts @@ -1,6 +1,6 @@ import { imessageOutbound } from "../../test/channel-outbounds.js"; import type { ChannelOutboundAdapter, ChannelPlugin } from "../channels/plugins/types.js"; -import { normalizeIMessageHandle } from "../plugin-sdk/imessage-targets.js"; +import { normalizeIMessageHandle } from "../plugin-sdk/imessage-policy.js"; import { collectStatusIssuesFromLastError } from "../plugin-sdk/status-helpers.js"; export const createIMessageTestPlugin = (params?: {