feat: add matrix channel plugin
This commit is contained in:
@@ -41,6 +41,7 @@ export function buildThreadingToolContext(params: {
|
||||
To: threadingTo,
|
||||
ReplyToId: sessionCtx.ReplyToId,
|
||||
ThreadLabel: sessionCtx.ThreadLabel,
|
||||
MessageThreadId: sessionCtx.MessageThreadId,
|
||||
},
|
||||
hasRepliedRef,
|
||||
}) ?? {}
|
||||
|
||||
@@ -216,9 +216,11 @@ export async function runReplyAgent(params: {
|
||||
abortedLastRun: false,
|
||||
};
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const topicId =
|
||||
typeof sessionCtx.MessageThreadId === "number" ? sessionCtx.MessageThreadId : undefined;
|
||||
const nextSessionFile = resolveSessionTranscriptPath(nextSessionId, agentId, topicId);
|
||||
const nextSessionFile = resolveSessionTranscriptPath(
|
||||
nextSessionId,
|
||||
agentId,
|
||||
sessionCtx.MessageThreadId,
|
||||
);
|
||||
nextEntry.sessionFile = nextSessionFile;
|
||||
activeSessionStore[sessionKey] = nextEntry;
|
||||
try {
|
||||
|
||||
@@ -23,7 +23,10 @@ export function buildInboundDedupeKey(ctx: MsgContext): string | null {
|
||||
if (!peerId) return null;
|
||||
const sessionKey = ctx.SessionKey?.trim() ?? "";
|
||||
const accountId = ctx.AccountId?.trim() ?? "";
|
||||
const threadId = typeof ctx.MessageThreadId === "number" ? String(ctx.MessageThreadId) : "";
|
||||
const threadId =
|
||||
ctx.MessageThreadId !== undefined && ctx.MessageThreadId !== null
|
||||
? String(ctx.MessageThreadId)
|
||||
: "";
|
||||
return [provider, accountId, sessionKey, peerId, threadId, messageId].filter(Boolean).join("|");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ export type FollowupRun = {
|
||||
originatingTo?: string;
|
||||
/** Provider account id (multi-account). */
|
||||
originatingAccountId?: string;
|
||||
/** Telegram forum topic thread id. */
|
||||
originatingThreadId?: number;
|
||||
/** Thread id for reply routing (Telegram topic id or Matrix thread event id). */
|
||||
originatingThreadId?: string | number;
|
||||
run: {
|
||||
agentId: string;
|
||||
agentDir: string;
|
||||
|
||||
@@ -27,8 +27,8 @@ export type RouteReplyParams = {
|
||||
sessionKey?: string;
|
||||
/** Provider account id (multi-account). */
|
||||
accountId?: string;
|
||||
/** Telegram message thread id (forum topics). */
|
||||
threadId?: number;
|
||||
/** Thread id for replies (Telegram topic id or Matrix thread event id). */
|
||||
threadId?: string | number;
|
||||
/** Config for provider-specific settings. */
|
||||
cfg: ClawdbotConfig;
|
||||
/** Optional abort signal for cooperative cancellation. */
|
||||
|
||||
@@ -53,8 +53,8 @@ export type MsgContext = {
|
||||
CommandAuthorized?: boolean;
|
||||
CommandSource?: "text" | "native";
|
||||
CommandTargetSessionKey?: string;
|
||||
/** Telegram forum topic thread ID. */
|
||||
MessageThreadId?: number;
|
||||
/** Thread identifier (Telegram topic id or Matrix thread event id). */
|
||||
MessageThreadId?: string | number;
|
||||
/** Telegram forum supergroup marker. */
|
||||
IsForum?: boolean;
|
||||
/**
|
||||
|
||||
@@ -103,11 +103,14 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: context.ReplyToId,
|
||||
hasRepliedRef,
|
||||
}),
|
||||
buildToolContext: ({ context, hasRepliedRef }) => {
|
||||
const threadId = context.MessageThreadId ?? context.ReplyToId;
|
||||
return {
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: threadId != null ? String(threadId) : undefined,
|
||||
hasRepliedRef,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
|
||||
@@ -6,10 +6,29 @@ export type ChannelPluginCatalogEntry = {
|
||||
install: {
|
||||
npmSpec: string;
|
||||
localPath?: string;
|
||||
defaultChoice?: "npm" | "local";
|
||||
};
|
||||
};
|
||||
|
||||
const CATALOG: ChannelPluginCatalogEntry[] = [
|
||||
{
|
||||
id: "matrix",
|
||||
meta: {
|
||||
id: "matrix",
|
||||
label: "Matrix",
|
||||
selectionLabel: "Matrix (plugin)",
|
||||
docsPath: "/channels/matrix",
|
||||
docsLabel: "matrix",
|
||||
blurb: "open protocol; install the plugin to enable.",
|
||||
order: 70,
|
||||
quickstartAllowFrom: true,
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@clawdbot/matrix",
|
||||
localPath: "extensions/matrix",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "zalo",
|
||||
meta: {
|
||||
|
||||
@@ -73,7 +73,7 @@ export type ChannelOutboundContext = {
|
||||
mediaUrl?: string;
|
||||
gifPlayback?: boolean;
|
||||
replyToId?: string | null;
|
||||
threadId?: number | null;
|
||||
threadId?: string | number | null;
|
||||
accountId?: string | null;
|
||||
deps?: OutboundSendDeps;
|
||||
};
|
||||
|
||||
@@ -31,6 +31,12 @@ export type ChannelSetupInput = {
|
||||
httpHost?: string;
|
||||
httpPort?: string;
|
||||
useEnv?: boolean;
|
||||
homeserver?: string;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
password?: string;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
};
|
||||
|
||||
export type ChannelStatusIssue = {
|
||||
@@ -196,6 +202,7 @@ export type ChannelThreadingContext = {
|
||||
To?: string;
|
||||
ReplyToId?: string;
|
||||
ThreadLabel?: string;
|
||||
MessageThreadId?: string | number;
|
||||
};
|
||||
|
||||
export type ChannelThreadingToolContext = {
|
||||
|
||||
@@ -32,6 +32,12 @@ const optionNamesAdd = [
|
||||
"httpHost",
|
||||
"httpPort",
|
||||
"useEnv",
|
||||
"homeserver",
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"deviceName",
|
||||
"initialSyncLimit",
|
||||
] as const;
|
||||
|
||||
const optionNamesRemove = ["channel", "account", "delete"] as const;
|
||||
@@ -115,6 +121,12 @@ export function registerChannelsCli(program: Command) {
|
||||
.option("--http-url <url>", "Signal HTTP daemon base URL")
|
||||
.option("--http-host <host>", "Signal HTTP host")
|
||||
.option("--http-port <port>", "Signal HTTP port")
|
||||
.option("--homeserver <url>", "Matrix homeserver URL")
|
||||
.option("--user-id <id>", "Matrix user ID")
|
||||
.option("--access-token <token>", "Matrix access token")
|
||||
.option("--password <password>", "Matrix password")
|
||||
.option("--device-name <name>", "Matrix device name")
|
||||
.option("--initial-sync-limit <n>", "Matrix initial sync limit")
|
||||
.option("--use-env", "Use env token (default account only)", false)
|
||||
.action(async (opts, command) => {
|
||||
try {
|
||||
|
||||
@@ -36,6 +36,12 @@ export function applyChannelAccountConfig(params: {
|
||||
httpHost?: string;
|
||||
httpPort?: string;
|
||||
useEnv?: boolean;
|
||||
homeserver?: string;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
password?: string;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
}): ClawdbotConfig {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
@@ -57,6 +63,12 @@ export function applyChannelAccountConfig(params: {
|
||||
httpHost: params.httpHost,
|
||||
httpPort: params.httpPort,
|
||||
useEnv: params.useEnv,
|
||||
homeserver: params.homeserver,
|
||||
userId: params.userId,
|
||||
accessToken: params.accessToken,
|
||||
password: params.password,
|
||||
deviceName: params.deviceName,
|
||||
initialSyncLimit: params.initialSyncLimit,
|
||||
};
|
||||
return apply({ cfg: params.cfg, accountId, input });
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ export type ChannelsAddOptions = {
|
||||
httpHost?: string;
|
||||
httpPort?: string;
|
||||
useEnv?: boolean;
|
||||
homeserver?: string;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
password?: string;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number | string;
|
||||
};
|
||||
|
||||
export async function channelsAddCommand(
|
||||
@@ -88,9 +94,9 @@ export async function channelsAddCommand(
|
||||
}
|
||||
|
||||
await writeConfigFile(nextConfig);
|
||||
await prompter.outro("Channels updated.");
|
||||
return;
|
||||
}
|
||||
await prompter.outro("Channels updated.");
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = normalizeChannelId(opts.channel);
|
||||
if (!channel) {
|
||||
@@ -109,6 +115,12 @@ export async function channelsAddCommand(
|
||||
plugin.setup.resolveAccountId?.({ cfg, accountId: opts.account }) ??
|
||||
normalizeAccountId(opts.account);
|
||||
const useEnv = opts.useEnv === true;
|
||||
const initialSyncLimit =
|
||||
typeof opts.initialSyncLimit === "number"
|
||||
? opts.initialSyncLimit
|
||||
: typeof opts.initialSyncLimit === "string" && opts.initialSyncLimit.trim()
|
||||
? Number.parseInt(opts.initialSyncLimit, 10)
|
||||
: undefined;
|
||||
const validationError = plugin.setup.validateInput?.({
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -127,6 +139,12 @@ export async function channelsAddCommand(
|
||||
httpUrl: opts.httpUrl,
|
||||
httpHost: opts.httpHost,
|
||||
httpPort: opts.httpPort,
|
||||
homeserver: opts.homeserver,
|
||||
userId: opts.userId,
|
||||
accessToken: opts.accessToken,
|
||||
password: opts.password,
|
||||
deviceName: opts.deviceName,
|
||||
initialSyncLimit,
|
||||
useEnv,
|
||||
},
|
||||
});
|
||||
@@ -154,6 +172,12 @@ export async function channelsAddCommand(
|
||||
httpUrl: opts.httpUrl,
|
||||
httpHost: opts.httpHost,
|
||||
httpPort: opts.httpPort,
|
||||
homeserver: opts.homeserver,
|
||||
userId: opts.userId,
|
||||
accessToken: opts.accessToken,
|
||||
password: opts.password,
|
||||
deviceName: opts.deviceName,
|
||||
initialSyncLimit,
|
||||
useEnv,
|
||||
});
|
||||
|
||||
|
||||
@@ -36,10 +36,16 @@ export function resolveDefaultSessionStorePath(agentId?: string): string {
|
||||
export function resolveSessionTranscriptPath(
|
||||
sessionId: string,
|
||||
agentId?: string,
|
||||
topicId?: number,
|
||||
topicId?: string | number,
|
||||
): string {
|
||||
const safeTopicId =
|
||||
typeof topicId === "string"
|
||||
? encodeURIComponent(topicId)
|
||||
: typeof topicId === "number"
|
||||
? String(topicId)
|
||||
: undefined;
|
||||
const fileName =
|
||||
topicId !== undefined ? `${sessionId}-topic-${topicId}.jsonl` : `${sessionId}.jsonl`;
|
||||
safeTopicId !== undefined ? `${sessionId}-topic-${safeTopicId}.jsonl` : `${sessionId}.jsonl`;
|
||||
return path.join(resolveAgentSessionsDir(agentId), fileName);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ import type { OutboundChannel } from "./targets.js";
|
||||
export type { NormalizedOutboundPayload } from "./payloads.js";
|
||||
export { normalizeOutboundPayloads } from "./payloads.js";
|
||||
|
||||
type SendMatrixMessage = (
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: { mediaUrl?: string; replyToId?: string; threadId?: string; timeoutMs?: number },
|
||||
) => Promise<{ messageId: string; roomId: string }>;
|
||||
|
||||
export type OutboundSendDeps = {
|
||||
sendWhatsApp?: typeof sendMessageWhatsApp;
|
||||
sendTelegram?: typeof sendMessageTelegram;
|
||||
@@ -25,6 +31,7 @@ export type OutboundSendDeps = {
|
||||
sendSlack?: typeof sendMessageSlack;
|
||||
sendSignal?: typeof sendMessageSignal;
|
||||
sendIMessage?: typeof sendMessageIMessage;
|
||||
sendMatrix?: SendMatrixMessage;
|
||||
sendMSTeams?: (
|
||||
to: string,
|
||||
text: string,
|
||||
@@ -37,6 +44,7 @@ export type OutboundDeliveryResult = {
|
||||
messageId: string;
|
||||
chatId?: string;
|
||||
channelId?: string;
|
||||
roomId?: string;
|
||||
conversationId?: string;
|
||||
timestamp?: number;
|
||||
toJid?: string;
|
||||
@@ -67,7 +75,7 @@ async function createChannelHandler(params: {
|
||||
to: string;
|
||||
accountId?: string;
|
||||
replyToId?: string | null;
|
||||
threadId?: number | null;
|
||||
threadId?: string | number | null;
|
||||
deps?: OutboundSendDeps;
|
||||
gifPlayback?: boolean;
|
||||
}): Promise<ChannelHandler> {
|
||||
@@ -99,7 +107,7 @@ function createPluginHandler(params: {
|
||||
to: string;
|
||||
accountId?: string;
|
||||
replyToId?: string | null;
|
||||
threadId?: number | null;
|
||||
threadId?: string | number | null;
|
||||
deps?: OutboundSendDeps;
|
||||
gifPlayback?: boolean;
|
||||
}): ChannelHandler | null {
|
||||
@@ -144,7 +152,7 @@ export async function deliverOutboundPayloads(params: {
|
||||
accountId?: string;
|
||||
payloads: ReplyPayload[];
|
||||
replyToId?: string | null;
|
||||
threadId?: number | null;
|
||||
threadId?: string | number | null;
|
||||
deps?: OutboundSendDeps;
|
||||
gifPlayback?: boolean;
|
||||
abortSignal?: AbortSignal;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type OutboundDeliveryJson = {
|
||||
mediaUrl: string | null;
|
||||
chatId?: string;
|
||||
channelId?: string;
|
||||
roomId?: string;
|
||||
conversationId?: string;
|
||||
timestamp?: number;
|
||||
toJid?: string;
|
||||
@@ -20,6 +21,7 @@ type OutboundDeliveryMeta = {
|
||||
messageId?: string;
|
||||
chatId?: string;
|
||||
channelId?: string;
|
||||
roomId?: string;
|
||||
conversationId?: string;
|
||||
timestamp?: number;
|
||||
toJid?: string;
|
||||
@@ -42,6 +44,7 @@ export function formatOutboundDeliverySummary(
|
||||
|
||||
if ("chatId" in result) return `${base} (chat ${result.chatId})`;
|
||||
if ("channelId" in result) return `${base} (channel ${result.channelId})`;
|
||||
if ("roomId" in result) return `${base} (room ${result.roomId})`;
|
||||
if ("conversationId" in result) return `${base} (conversation ${result.conversationId})`;
|
||||
return base;
|
||||
}
|
||||
@@ -69,6 +72,9 @@ export function buildOutboundDeliveryJson(params: {
|
||||
if (result && "channelId" in result && result.channelId !== undefined) {
|
||||
payload.channelId = result.channelId;
|
||||
}
|
||||
if (result && "roomId" in result && result.roomId !== undefined) {
|
||||
payload.roomId = result.roomId;
|
||||
}
|
||||
if (result && "conversationId" in result && result.conversationId !== undefined) {
|
||||
payload.conversationId = result.conversationId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user