feat: add providers CLI and multi-account onboarding
This commit is contained in:
74
src/imessage/accounts.ts
Normal file
74
src/imessage/accounts.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { IMessageAccountConfig } from "../config/types.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
|
||||
export type ResolvedIMessageAccount = {
|
||||
accountId: string;
|
||||
enabled: boolean;
|
||||
name?: string;
|
||||
config: IMessageAccountConfig;
|
||||
};
|
||||
|
||||
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const accounts = cfg.imessage?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return [];
|
||||
return Object.keys(accounts).filter(Boolean);
|
||||
}
|
||||
|
||||
export function listIMessageAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const ids = listConfiguredAccountIds(cfg);
|
||||
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
||||
return ids.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function resolveDefaultIMessageAccountId(cfg: ClawdbotConfig): string {
|
||||
const ids = listIMessageAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
function resolveAccountConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
): IMessageAccountConfig | undefined {
|
||||
const accounts = cfg.imessage?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return undefined;
|
||||
return accounts[accountId] as IMessageAccountConfig | undefined;
|
||||
}
|
||||
|
||||
function mergeIMessageAccountConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
): IMessageAccountConfig {
|
||||
const { accounts: _ignored, ...base } = (cfg.imessage ??
|
||||
{}) as IMessageAccountConfig & { accounts?: unknown };
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
return { ...base, ...account };
|
||||
}
|
||||
|
||||
export function resolveIMessageAccount(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
}): ResolvedIMessageAccount {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const baseEnabled = params.cfg.imessage?.enabled !== false;
|
||||
const merged = mergeIMessageAccountConfig(params.cfg, accountId);
|
||||
const accountEnabled = merged.enabled !== false;
|
||||
return {
|
||||
accountId,
|
||||
enabled: baseEnabled && accountEnabled,
|
||||
name: merged.name?.trim() || undefined,
|
||||
config: merged,
|
||||
};
|
||||
}
|
||||
|
||||
export function listEnabledIMessageAccounts(
|
||||
cfg: ClawdbotConfig,
|
||||
): ResolvedIMessageAccount[] {
|
||||
return listIMessageAccountIds(cfg)
|
||||
.map((accountId) => resolveIMessageAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../auto-reply/reply/mentions.js";
|
||||
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveProviderGroupPolicy,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
} from "../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveIMessageAccount } from "./accounts.js";
|
||||
import { createIMessageRpcClient } from "./client.js";
|
||||
import { sendMessageIMessage } from "./send.js";
|
||||
import {
|
||||
@@ -56,6 +58,8 @@ export type MonitorIMessageOpts = {
|
||||
abortSignal?: AbortSignal;
|
||||
cliPath?: string;
|
||||
dbPath?: string;
|
||||
accountId?: string;
|
||||
config?: ClawdbotConfig;
|
||||
allowFrom?: Array<string | number>;
|
||||
groupAllowFrom?: Array<string | number>;
|
||||
includeAttachments?: boolean;
|
||||
@@ -75,32 +79,21 @@ function resolveRuntime(opts: MonitorIMessageOpts): RuntimeEnv {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAllowFrom(opts: MonitorIMessageOpts): string[] {
|
||||
const cfg = loadConfig();
|
||||
const raw = opts.allowFrom ?? cfg.imessage?.allowFrom ?? [];
|
||||
return raw.map((entry) => String(entry).trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function resolveGroupAllowFrom(opts: MonitorIMessageOpts): string[] {
|
||||
const cfg = loadConfig();
|
||||
const raw =
|
||||
opts.groupAllowFrom ??
|
||||
cfg.imessage?.groupAllowFrom ??
|
||||
(cfg.imessage?.allowFrom && cfg.imessage.allowFrom.length > 0
|
||||
? cfg.imessage.allowFrom
|
||||
: []);
|
||||
return raw.map((entry) => String(entry).trim()).filter(Boolean);
|
||||
function normalizeAllowList(list?: Array<string | number>) {
|
||||
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
async function deliverReplies(params: {
|
||||
replies: ReplyPayload[];
|
||||
target: string;
|
||||
client: Awaited<ReturnType<typeof createIMessageRpcClient>>;
|
||||
accountId?: string;
|
||||
runtime: RuntimeEnv;
|
||||
maxBytes: number;
|
||||
textLimit: number;
|
||||
}) {
|
||||
const { replies, target, client, runtime, maxBytes, textLimit } = params;
|
||||
const { replies, target, client, runtime, maxBytes, textLimit, accountId } =
|
||||
params;
|
||||
for (const payload of replies) {
|
||||
const mediaList =
|
||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
@@ -108,7 +101,11 @@ async function deliverReplies(params: {
|
||||
if (!text && mediaList.length === 0) continue;
|
||||
if (mediaList.length === 0) {
|
||||
for (const chunk of chunkText(text, textLimit)) {
|
||||
await sendMessageIMessage(target, chunk, { maxBytes, client });
|
||||
await sendMessageIMessage(target, chunk, {
|
||||
maxBytes,
|
||||
client,
|
||||
accountId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let first = true;
|
||||
@@ -119,6 +116,7 @@ async function deliverReplies(params: {
|
||||
mediaUrl: url,
|
||||
maxBytes,
|
||||
client,
|
||||
accountId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -130,17 +128,32 @@ export async function monitorIMessageProvider(
|
||||
opts: MonitorIMessageOpts = {},
|
||||
): Promise<void> {
|
||||
const runtime = resolveRuntime(opts);
|
||||
const cfg = loadConfig();
|
||||
const textLimit = resolveTextChunkLimit(cfg, "imessage");
|
||||
const allowFrom = resolveAllowFrom(opts);
|
||||
const groupAllowFrom = resolveGroupAllowFrom(opts);
|
||||
const groupPolicy = cfg.imessage?.groupPolicy ?? "open";
|
||||
const dmPolicy = cfg.imessage?.dmPolicy ?? "pairing";
|
||||
const cfg = opts.config ?? loadConfig();
|
||||
const accountInfo = resolveIMessageAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const imessageCfg = accountInfo.config;
|
||||
const textLimit = resolveTextChunkLimit(
|
||||
cfg,
|
||||
"imessage",
|
||||
accountInfo.accountId,
|
||||
);
|
||||
const allowFrom = normalizeAllowList(opts.allowFrom ?? imessageCfg.allowFrom);
|
||||
const groupAllowFrom = normalizeAllowList(
|
||||
opts.groupAllowFrom ??
|
||||
imessageCfg.groupAllowFrom ??
|
||||
(imessageCfg.allowFrom && imessageCfg.allowFrom.length > 0
|
||||
? imessageCfg.allowFrom
|
||||
: []),
|
||||
);
|
||||
const groupPolicy = imessageCfg.groupPolicy ?? "open";
|
||||
const dmPolicy = imessageCfg.dmPolicy ?? "pairing";
|
||||
const mentionRegexes = buildMentionRegexes(cfg);
|
||||
const includeAttachments =
|
||||
opts.includeAttachments ?? cfg.imessage?.includeAttachments ?? false;
|
||||
opts.includeAttachments ?? imessageCfg.includeAttachments ?? false;
|
||||
const mediaMaxBytes =
|
||||
(opts.mediaMaxMb ?? cfg.imessage?.mediaMaxMb ?? 16) * 1024 * 1024;
|
||||
(opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024;
|
||||
|
||||
const handleMessage = async (raw: unknown) => {
|
||||
const params = raw as { message?: IMessagePayload | null };
|
||||
@@ -202,6 +215,7 @@ export async function monitorIMessageProvider(
|
||||
const groupListPolicy = resolveProviderGroupPolicy({
|
||||
cfg,
|
||||
provider: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
groupId,
|
||||
});
|
||||
if (groupListPolicy.allowlistEnabled && !groupListPolicy.allowed) {
|
||||
@@ -254,6 +268,7 @@ export async function monitorIMessageProvider(
|
||||
{
|
||||
client,
|
||||
maxBytes: mediaMaxBytes,
|
||||
accountId: accountInfo.accountId,
|
||||
...(chatId ? { chatId } : {}),
|
||||
},
|
||||
);
|
||||
@@ -279,6 +294,7 @@ export async function monitorIMessageProvider(
|
||||
const requireMention = resolveProviderGroupRequireMention({
|
||||
cfg,
|
||||
provider: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
groupId,
|
||||
requireMentionOverride: opts.requireMention,
|
||||
overrideOrder: "before-config",
|
||||
@@ -344,6 +360,7 @@ export async function monitorIMessageProvider(
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
peer: {
|
||||
kind: isGroup ? "group" : "dm",
|
||||
id: isGroup
|
||||
@@ -410,6 +427,7 @@ export async function monitorIMessageProvider(
|
||||
replies: [payload],
|
||||
target: ctxPayload.To,
|
||||
client,
|
||||
accountId: accountInfo.accountId,
|
||||
runtime,
|
||||
maxBytes: mediaMaxBytes,
|
||||
textLimit,
|
||||
@@ -431,8 +449,8 @@ export async function monitorIMessageProvider(
|
||||
};
|
||||
|
||||
const client = await createIMessageRpcClient({
|
||||
cliPath: opts.cliPath ?? cfg.imessage?.cliPath,
|
||||
dbPath: opts.dbPath ?? cfg.imessage?.dbPath,
|
||||
cliPath: opts.cliPath ?? imessageCfg.cliPath,
|
||||
dbPath: opts.dbPath ?? imessageCfg.dbPath,
|
||||
runtime,
|
||||
onNotification: (msg) => {
|
||||
if (msg.method === "message") {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { loadConfig } from "../config/config.js";
|
||||
import { mediaKindFromMime } from "../media/constants.js";
|
||||
import { saveMediaBuffer } from "../media/store.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
import { resolveIMessageAccount } from "./accounts.js";
|
||||
import { createIMessageRpcClient, type IMessageRpcClient } from "./client.js";
|
||||
import {
|
||||
formatIMessageChatTarget,
|
||||
@@ -14,6 +15,7 @@ export type IMessageSendOpts = {
|
||||
dbPath?: string;
|
||||
service?: IMessageService;
|
||||
region?: string;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
maxBytes?: number;
|
||||
timeoutMs?: number;
|
||||
@@ -25,28 +27,6 @@ export type IMessageSendResult = {
|
||||
messageId: string;
|
||||
};
|
||||
|
||||
function resolveCliPath(explicit?: string): string {
|
||||
const cfg = loadConfig();
|
||||
return explicit?.trim() || cfg.imessage?.cliPath?.trim() || "imsg";
|
||||
}
|
||||
|
||||
function resolveDbPath(explicit?: string): string | undefined {
|
||||
const cfg = loadConfig();
|
||||
return explicit?.trim() || cfg.imessage?.dbPath?.trim() || undefined;
|
||||
}
|
||||
|
||||
function resolveService(explicit?: IMessageService): IMessageService {
|
||||
const cfg = loadConfig();
|
||||
return (
|
||||
explicit || (cfg.imessage?.service as IMessageService | undefined) || "auto"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRegion(explicit?: string): string {
|
||||
const cfg = loadConfig();
|
||||
return explicit?.trim() || cfg.imessage?.region?.trim() || "US";
|
||||
}
|
||||
|
||||
async function resolveAttachment(
|
||||
mediaUrl: string,
|
||||
maxBytes: number,
|
||||
@@ -66,15 +46,28 @@ export async function sendMessageIMessage(
|
||||
text: string,
|
||||
opts: IMessageSendOpts = {},
|
||||
): Promise<IMessageSendResult> {
|
||||
const cliPath = resolveCliPath(opts.cliPath);
|
||||
const dbPath = resolveDbPath(opts.dbPath);
|
||||
const cfg = loadConfig();
|
||||
const account = resolveIMessageAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const cliPath =
|
||||
opts.cliPath?.trim() || account.config.cliPath?.trim() || "imsg";
|
||||
const dbPath = opts.dbPath?.trim() || account.config.dbPath?.trim();
|
||||
const target = parseIMessageTarget(
|
||||
opts.chatId ? formatIMessageChatTarget(opts.chatId) : to,
|
||||
);
|
||||
const service =
|
||||
opts.service ?? (target.kind === "handle" ? target.service : undefined);
|
||||
const region = resolveRegion(opts.region);
|
||||
const maxBytes = opts.maxBytes ?? 16 * 1024 * 1024;
|
||||
opts.service ??
|
||||
(target.kind === "handle" ? target.service : undefined) ??
|
||||
(account.config.service as IMessageService | undefined);
|
||||
const region = opts.region?.trim() || account.config.region?.trim() || "US";
|
||||
const maxBytes =
|
||||
typeof opts.maxBytes === "number"
|
||||
? opts.maxBytes
|
||||
: typeof account.config.mediaMaxMb === "number"
|
||||
? account.config.mediaMaxMb * 1024 * 1024
|
||||
: 16 * 1024 * 1024;
|
||||
let message = text ?? "";
|
||||
let filePath: string | undefined;
|
||||
|
||||
@@ -94,7 +87,7 @@ export async function sendMessageIMessage(
|
||||
|
||||
const params: Record<string, unknown> = {
|
||||
text: message,
|
||||
service: resolveService(service),
|
||||
service: (service || "auto") as IMessageService,
|
||||
region,
|
||||
};
|
||||
if (filePath) params.file = filePath;
|
||||
|
||||
Reference in New Issue
Block a user