mirror of https://github.com/openclaw/openclaw.git
282 lines
9.3 KiB
TypeScript
282 lines
9.3 KiB
TypeScript
import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit";
|
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
|
|
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
import { resolveOutboundSendDep } 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 {
|
|
chunkTextForOutbound,
|
|
collectStatusIssuesFromLastError,
|
|
DEFAULT_ACCOUNT_ID,
|
|
formatTrimmedAllowFromEntries,
|
|
normalizeIMessageMessagingTarget,
|
|
type ChannelPlugin,
|
|
} from "../runtime-api.js";
|
|
import { resolveIMessageAccount, type ResolvedIMessageAccount } from "./accounts.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,
|
|
imessageConfigAdapter,
|
|
imessageSecurityAdapter,
|
|
imessageSetupWizard,
|
|
} from "./shared.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,
|
|
},
|
|
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 (await loadIMessageChannelRuntime()).probeIMessageAccount({
|
|
timeoutMs,
|
|
cliPath: account.config.cliPath,
|
|
dbPath: account.config.dbPath,
|
|
}),
|
|
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 }) =>
|
|
await (await loadIMessageChannelRuntime()).notifyIMessageApproval(id),
|
|
},
|
|
},
|
|
security: imessageSecurityAdapter,
|
|
outbound: {
|
|
base: {
|
|
deliveryMode: "direct",
|
|
chunker: chunkTextForOutbound,
|
|
chunkerMode: "text",
|
|
textChunkLimit: 4000,
|
|
},
|
|
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,
|
|
}),
|
|
},
|
|
},
|
|
});
|