import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit" ;
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core" ;
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared" ;
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime" ;
import { sanitizeForPlainText } from "openclaw/plugin-sdk/outbound-runtime" ;
import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing" ;
import {
createComputedAccountStatusAdapter,
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/status-helpers" ;
import { resolveIMessageAccount, type ResolvedIMessageAccount } from "./accounts.js" ;
import {
chunkTextForOutbound,
collectStatusIssuesFromLastError,
DEFAULT_ACCOUNT_ID,
formatTrimmedAllowFromEntries,
normalizeIMessageMessagingTarget,
type ChannelPlugin,
} from "./channel-api.js" ;
import { createIMessageConversationBindingManager } from "./conversation-bindings.js" ;
import {
matchIMessageAcpConversation,
normalizeIMessageAcpConversationId,
resolveIMessageConversationIdFromTarget,
} from "./conversation-id.js" ;
import {
resolveIMessageGroupRequireMention,
resolveIMessageGroupToolPolicy,
} from "./group-policy.js" ;
import type { IMessageProbe } from "./probe.js" ;
import { imessageSetupAdapter } from "./setup-core.js" ;
import {
createIMessagePluginBase,
imessageSecurityAdapter,
imessageSetupWizard,
} from "./shared.js" ;
import { probeIMessageStatusAccount } from "./status-core.js" ;
import {
inferIMessageTargetChatType,
looksLikeIMessageExplicitTargetId,
normalizeIMessageHandle,
parseIMessageTarget,
} from "./targets.js" ;
const loadIMessageChannelRuntime = createLazyRuntimeModule(() => import ("./channel.runtime.js" ));
function buildIMessageBaseSessionKey(params: {
cfg: Parameters<typeof resolveIMessageAccount>[0 ]["cfg" ];
agentId: string;
accountId?: string | null ;
peer: RoutePeer;
}) {
return buildOutboundBaseSessionKey({ ...params, channel: "imessage" });
}
function resolveIMessageOutboundSessionRoute(params: {
cfg: Parameters<typeof resolveIMessageAccount>[0 ]["cfg" ];
agentId: string;
accountId?: string | null ;
target: string;
}) {
const parsed = parseIMessageTarget(params.target);
if (parsed.kind === "handle" ) {
const handle = normalizeIMessageHandle(parsed.to);
if (!handle) {
return null ;
}
const peer: RoutePeer = { kind: "direct" , id: handle };
const baseSessionKey = buildIMessageBaseSessionKey({
cfg: params.cfg,
agentId: params.agentId,
accountId: params.accountId,
peer,
});
return {
sessionKey: baseSessionKey,
baseSessionKey,
peer,
chatType: "direct" as const ,
from: `imessage:${handle}`,
to: `imessage:${handle}`,
};
}
const peerId =
parsed.kind === "chat_id"
? String(parsed.chatId)
: parsed.kind === "chat_guid"
? parsed.chatGuid
: parsed.chatIdentifier;
if (!peerId) {
return null ;
}
const peer: RoutePeer = { kind: "group" , id: peerId };
const baseSessionKey = buildIMessageBaseSessionKey({
cfg: params.cfg,
agentId: params.agentId,
accountId: params.accountId,
peer,
});
const toPrefix =
parsed.kind === "chat_id"
? "chat_id"
: parsed.kind === "chat_guid"
? "chat_guid"
: "chat_identifier" ;
return {
sessionKey: baseSessionKey,
baseSessionKey,
peer,
chatType: "group" as const ,
from: `imessage:group:${peerId}`,
to: `${toPrefix}:${peerId}`,
};
}
export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount, IMessageProbe> =
createChatChannelPlugin<ResolvedIMessageAccount, IMessageProbe>({
base: {
...createIMessagePluginBase({
setupWizard: imessageSetupWizard,
setup: imessageSetupAdapter,
}),
allowlist: buildDmGroupAccountAllowlistAdapter({
channelId: "imessage" ,
resolveAccount: resolveIMessageAccount,
normalize: ({ values }) => formatTrimmedAllowFromEntries(values),
resolveDmAllowFrom: (account) => account.config.allowFrom,
resolveGroupAllowFrom: (account) => account.config.groupAllowFrom,
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveGroupPolicy: (account) => account.config.groupPolicy,
}),
groups: {
resolveRequireMention: resolveIMessageGroupRequireMention,
resolveToolPolicy: resolveIMessageGroupToolPolicy,
},
doctor: {
groupAllowFromFallbackToAllowFrom: false ,
},
conversationBindings: {
supportsCurrentConversationBinding: true ,
createManager: ({ cfg, accountId }) =>
createIMessageConversationBindingManager({
cfg,
accountId: accountId ?? undefined,
}),
},
bindings: {
compileConfiguredBinding: ({ conversationId }) =>
normalizeIMessageAcpConversationId(conversationId),
matchInboundConversation: ({ compiledBinding, conversationId }) =>
matchIMessageAcpConversation({
bindingConversationId: compiledBinding.conversationId,
conversationId,
}),
resolveCommandConversation: ({ originatingTo, commandTo, fallbackTo }) => {
const conversationId =
resolveIMessageConversationIdFromTarget(originatingTo ?? "" ) ??
resolveIMessageConversationIdFromTarget(commandTo ?? "" ) ??
resolveIMessageConversationIdFromTarget(fallbackTo ?? "" );
return conversationId ? { conversationId } : null ;
},
},
messaging: {
normalizeTarget: normalizeIMessageMessagingTarget,
inferTargetChatType: ({ to }) => inferIMessageTargetChatType(to),
resolveOutboundSessionRoute: (params) => resolveIMessageOutboundSessionRoute(params),
targetResolver: {
looksLikeId: looksLikeIMessageExplicitTargetId,
hint: "<handle|chat_id:ID>" ,
resolveTarget: async ({ normalized }) => {
const to = normalized?.trim();
if (!to) {
return null ;
}
const chatType = inferIMessageTargetChatType(to);
if (!chatType) {
return null ;
}
return {
to,
kind: chatType === "direct" ? "user" : "group" ,
source: "normalized" as const ,
};
},
},
},
status: createComputedAccountStatusAdapter<ResolvedIMessageAccount, IMessageProbe>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
cliPath: null ,
dbPath: null ,
}),
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("imessage" , accounts),
buildChannelSummary: ({ snapshot }) =>
buildPassiveProbedChannelStatusSummary(snapshot, {
cliPath: snapshot.cliPath ?? null ,
dbPath: snapshot.dbPath ?? null ,
}),
probeAccount: async ({ account, timeoutMs }) =>
await probeIMessageStatusAccount({
account,
timeoutMs,
probeIMessageAccount: async (params) =>
await (await loadIMessageChannelRuntime()).probeIMessageAccount(params),
}),
resolveAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
extra: {
cliPath: runtime?.cliPath ?? account.config.cliPath ?? null ,
dbPath: runtime?.dbPath ?? account.config.dbPath ?? null ,
},
}),
resolveAccountState: ({ enabled }) => (enabled ? "enabled" : "disabled" ),
}),
gateway: {
startAccount: async (ctx) => {
const conversationBindings = createIMessageConversationBindingManager({
cfg: ctx.cfg,
accountId: ctx.accountId,
});
try {
return await (await loadIMessageChannelRuntime()).startIMessageGatewayAccount(ctx);
} finally {
conversationBindings.stop();
}
},
},
},
pairing: {
text: {
idLabel: "imessageSenderId" ,
message: "OpenClaw: your access has been approved." ,
notify: async ({ id, cfg }) =>
await (await loadIMessageChannelRuntime()).notifyIMessageApproval({ id, cfg }),
},
},
security: imessageSecurityAdapter,
outbound: {
base: {
deliveryMode: "direct" ,
chunker: chunkTextForOutbound,
chunkerMode: "text" ,
textChunkLimit: 4000 ,
sanitizeText: ({ text }) => sanitizeForPlainText(text),
},
attachedResults: {
channel: "imessage" ,
sendText: async ({ cfg, to, text, accountId, deps, replyToId }) =>
await (
await loadIMessageChannelRuntime()
).sendIMessageOutbound({
cfg,
to,
text,
accountId: accountId ?? undefined,
deps,
replyToId: replyToId ?? undefined,
}),
sendMedia: async ({
cfg,
to,
text,
mediaUrl,
mediaLocalRoots,
accountId,
deps,
replyToId,
}) =>
await (
await loadIMessageChannelRuntime()
).sendIMessageOutbound({
cfg,
to,
text,
mediaUrl,
mediaLocalRoots,
accountId: accountId ?? undefined,
deps,
replyToId: replyToId ?? undefined,
}),
},
},
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.8 Sekunden
(vorverarbeitet am 2026-06-07)
¤
*© Formatika GbR, Deutschland