fix: add per-channel markdown table conversion (#1495) (thanks @odysseus0)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -25,6 +26,7 @@ const bluebubblesGroupConfigSchema = z.object({
|
||||
const bluebubblesAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
serverUrl: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
webhookPath: z.string().optional(),
|
||||
|
||||
@@ -99,6 +99,8 @@ function createMockRuntime(): PluginRuntime {
|
||||
chunkText: vi.fn() as unknown as PluginRuntime["channel"]["text"]["chunkText"],
|
||||
resolveTextChunkLimit: vi.fn(() => 4000) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
|
||||
hasControlCommand: mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
|
||||
resolveMarkdownTableMode: vi.fn(() => "code") as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
|
||||
convertMarkdownTables: vi.fn((text: string) => text) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher: mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
|
||||
@@ -1662,9 +1662,15 @@ async function processMessage(
|
||||
? [payload.mediaUrl]
|
||||
: [];
|
||||
if (mediaList.length > 0) {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
let first = true;
|
||||
for (const mediaUrl of mediaList) {
|
||||
const caption = first ? payload.text : undefined;
|
||||
const caption = first ? text : undefined;
|
||||
first = false;
|
||||
const result = await sendBlueBubblesMedia({
|
||||
cfg: config,
|
||||
@@ -1686,8 +1692,14 @@ async function processMessage(
|
||||
account.config.textChunkLimit && account.config.textChunkLimit > 0
|
||||
? account.config.textChunkLimit
|
||||
: DEFAULT_TEXT_LIMIT;
|
||||
const chunks = core.channel.text.chunkMarkdownText(payload.text ?? "", textLimit);
|
||||
if (!chunks.length && payload.text) chunks.push(payload.text);
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
if (!chunks.length) return;
|
||||
for (const chunk of chunks) {
|
||||
const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -35,6 +36,7 @@ const matrixRoomSchema = z
|
||||
export const MatrixConfigSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
homeserver: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
|
||||
@@ -548,6 +548,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
}
|
||||
|
||||
let didSendReply = false;
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
accountId: route.accountId,
|
||||
});
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
core.channel.reply.createReplyDispatcherWithTyping({
|
||||
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
@@ -562,6 +567,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
textLimit,
|
||||
replyToMode,
|
||||
threadId: threadTarget,
|
||||
accountId: route.accountId,
|
||||
tableMode,
|
||||
});
|
||||
didSendReply = true;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import type { ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import { sendMessageMatrix } from "../send.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
@@ -12,8 +12,17 @@ export async function deliverMatrixReplies(params: {
|
||||
textLimit: number;
|
||||
replyToMode: "off" | "first" | "all";
|
||||
threadId?: string;
|
||||
accountId?: string;
|
||||
tableMode?: MarkdownTableMode;
|
||||
}): Promise<void> {
|
||||
const core = getMatrixRuntime();
|
||||
const tableMode =
|
||||
params.tableMode ??
|
||||
core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: core.config.loadConfig(),
|
||||
channel: "matrix",
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const logVerbose = (message: string) => {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
params.runtime.log?.(message);
|
||||
@@ -33,6 +42,8 @@ export async function deliverMatrixReplies(params: {
|
||||
}
|
||||
const replyToIdRaw = reply.replyToId?.trim();
|
||||
const replyToId = params.threadId || params.replyToMode === "off" ? undefined : replyToIdRaw;
|
||||
const rawText = reply.text ?? "";
|
||||
const text = core.channel.text.convertMarkdownTables(rawText, tableMode);
|
||||
const mediaList = reply.mediaUrls?.length
|
||||
? reply.mediaUrls
|
||||
: reply.mediaUrl
|
||||
@@ -43,13 +54,14 @@ export async function deliverMatrixReplies(params: {
|
||||
Boolean(id) && (params.replyToMode === "all" || !hasReplied);
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
for (const chunk of core.channel.text.chunkMarkdownText(reply.text ?? "", chunkLimit)) {
|
||||
for (const chunk of core.channel.text.chunkMarkdownText(text, chunkLimit)) {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed) continue;
|
||||
await sendMessageMatrix(params.roomId, trimmed, {
|
||||
client: params.client,
|
||||
replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
|
||||
threadId: params.threadId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
if (shouldIncludeReply(replyToId)) {
|
||||
hasReplied = true;
|
||||
@@ -60,13 +72,14 @@ export async function deliverMatrixReplies(params: {
|
||||
|
||||
let first = true;
|
||||
for (const mediaUrl of mediaList) {
|
||||
const caption = first ? (reply.text ?? "") : "";
|
||||
const caption = first ? text : "";
|
||||
await sendMessageMatrix(params.roomId, caption, {
|
||||
client: params.client,
|
||||
mediaUrl,
|
||||
replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
|
||||
threadId: params.threadId,
|
||||
audioAsVoice: reply.audioAsVoice,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
if (shouldIncludeReply(replyToId)) {
|
||||
hasReplied = true;
|
||||
|
||||
@@ -43,6 +43,8 @@ const runtimeStub = {
|
||||
text: {
|
||||
resolveTextChunkLimit: () => 4000,
|
||||
chunkMarkdownText: (text: string) => (text ? [text] : []),
|
||||
resolveMarkdownTableMode: () => "code",
|
||||
convertMarkdownTables: (text: string) => text,
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime;
|
||||
|
||||
@@ -50,9 +50,18 @@ export async function sendMessageMatrix(
|
||||
try {
|
||||
const roomId = await resolveMatrixRoomId(client, to);
|
||||
const cfg = getCore().config.loadConfig();
|
||||
const tableMode = getCore().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const convertedMessage = getCore().channel.text.convertMarkdownTables(
|
||||
trimmedMessage,
|
||||
tableMode,
|
||||
);
|
||||
const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix");
|
||||
const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
|
||||
const chunks = getCore().channel.text.chunkMarkdownText(trimmedMessage, chunkLimit);
|
||||
const chunks = getCore().channel.text.chunkMarkdownText(convertedMessage, chunkLimit);
|
||||
const threadId = normalizeThreadId(opts.threadId);
|
||||
const relation = threadId
|
||||
? buildThreadRelation(threadId, opts.replyToId)
|
||||
|
||||
@@ -87,6 +87,7 @@ export type MatrixSendResult = {
|
||||
export type MatrixSendOpts = {
|
||||
client?: import("matrix-bot-sdk").MatrixClient;
|
||||
mediaUrl?: string;
|
||||
accountId?: string;
|
||||
replyToId?: string;
|
||||
threadId?: string | number | null;
|
||||
timeoutMs?: number;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
BlockStreamingCoalesceSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
@@ -11,6 +12,7 @@ const MattermostAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
botToken: z.string().optional(),
|
||||
|
||||
@@ -707,6 +707,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, {
|
||||
fallbackLimit: account.textChunkLimit ?? 4000,
|
||||
});
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "mattermost",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
|
||||
let prefixContext: ResponsePrefixContext = {
|
||||
identityName: resolveIdentityName(cfg, route.agentId),
|
||||
@@ -720,7 +725,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload: ReplyPayload) => {
|
||||
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const text = payload.text ?? "";
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
if (mediaUrls.length === 0) {
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
for (const chunk of chunks.length > 0 ? chunks : [text]) {
|
||||
|
||||
@@ -181,6 +181,15 @@ export async function sendMessageMattermost(
|
||||
}
|
||||
}
|
||||
|
||||
if (message) {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "mattermost",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
message = core.channel.text.convertMarkdownTables(message, tableMode);
|
||||
}
|
||||
|
||||
if (!message && (!fileIds || fileIds.length === 0)) {
|
||||
if (uploadError) {
|
||||
throw new Error(`Mattermost media upload failed: ${uploadError.message}`);
|
||||
@@ -205,4 +214,4 @@ export async function sendMessageMattermost(
|
||||
messageId: post.id ?? "unknown",
|
||||
channelId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const runtimeStub = {
|
||||
}
|
||||
return chunks;
|
||||
},
|
||||
resolveMarkdownTableMode: () => "code",
|
||||
convertMarkdownTables: (text: string) => text,
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime;
|
||||
@@ -34,6 +36,7 @@ describe("msteams messenger", () => {
|
||||
it("filters silent replies", () => {
|
||||
const messages = renderReplyPayloadsToMessages([{ text: SILENT_REPLY_TOKEN }], {
|
||||
textChunkLimit: 4000,
|
||||
tableMode: "code",
|
||||
});
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
@@ -41,7 +44,7 @@ describe("msteams messenger", () => {
|
||||
it("filters silent reply prefixes", () => {
|
||||
const messages = renderReplyPayloadsToMessages(
|
||||
[{ text: `${SILENT_REPLY_TOKEN} -- ignored` }],
|
||||
{ textChunkLimit: 4000 },
|
||||
{ textChunkLimit: 4000, tableMode: "code" },
|
||||
);
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
@@ -49,7 +52,7 @@ describe("msteams messenger", () => {
|
||||
it("splits media into separate messages by default", () => {
|
||||
const messages = renderReplyPayloadsToMessages(
|
||||
[{ text: "hi", mediaUrl: "https://example.com/a.png" }],
|
||||
{ textChunkLimit: 4000 },
|
||||
{ textChunkLimit: 4000, tableMode: "code" },
|
||||
);
|
||||
expect(messages).toEqual([{ text: "hi" }, { mediaUrl: "https://example.com/a.png" }]);
|
||||
});
|
||||
@@ -57,7 +60,7 @@ describe("msteams messenger", () => {
|
||||
it("supports inline media mode", () => {
|
||||
const messages = renderReplyPayloadsToMessages(
|
||||
[{ text: "hi", mediaUrl: "https://example.com/a.png" }],
|
||||
{ textChunkLimit: 4000, mediaMode: "inline" },
|
||||
{ textChunkLimit: 4000, mediaMode: "inline", tableMode: "code" },
|
||||
);
|
||||
expect(messages).toEqual([{ text: "hi", mediaUrl: "https://example.com/a.png" }]);
|
||||
});
|
||||
@@ -66,6 +69,7 @@ describe("msteams messenger", () => {
|
||||
const long = "hello ".repeat(200);
|
||||
const messages = renderReplyPayloadsToMessages([{ text: long }], {
|
||||
textChunkLimit: 50,
|
||||
tableMode: "code",
|
||||
});
|
||||
expect(messages.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
isSilentReplyText,
|
||||
loadWebMedia,
|
||||
type MarkdownTableMode,
|
||||
type MSTeamsReplyStyle,
|
||||
type ReplyPayload,
|
||||
SILENT_REPLY_TOKEN,
|
||||
@@ -61,6 +62,7 @@ export type MSTeamsReplyRenderOptions = {
|
||||
textChunkLimit: number;
|
||||
chunkText?: boolean;
|
||||
mediaMode?: "split" | "inline";
|
||||
tableMode?: MarkdownTableMode;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -196,10 +198,19 @@ export function renderReplyPayloadsToMessages(
|
||||
const chunkLimit = Math.min(options.textChunkLimit, 4000);
|
||||
const chunkText = options.chunkText !== false;
|
||||
const mediaMode = options.mediaMode ?? "split";
|
||||
const tableMode =
|
||||
options.tableMode ??
|
||||
getMSTeamsRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg: getMSTeamsRuntime().config.loadConfig(),
|
||||
channel: "msteams",
|
||||
});
|
||||
|
||||
for (const payload of replies) {
|
||||
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const text = payload.text ?? "";
|
||||
const text = getMSTeamsRuntime().channel.text.convertMarkdownTables(
|
||||
payload.text ?? "",
|
||||
tableMode,
|
||||
);
|
||||
|
||||
if (!text && mediaList.length === 0) continue;
|
||||
|
||||
|
||||
@@ -53,10 +53,15 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
).responsePrefix,
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
|
||||
deliver: async (payload) => {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: params.cfg,
|
||||
channel: "msteams",
|
||||
});
|
||||
const messages = renderReplyPayloadsToMessages([payload], {
|
||||
textChunkLimit: params.textLimit,
|
||||
chunkText: true,
|
||||
mediaMode: "split",
|
||||
tableMode,
|
||||
});
|
||||
const mediaMaxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { extractFilename, extractMessageId } from "./media-helpers.js";
|
||||
import { buildConversationReference, sendMSTeamsMessages } from "./messenger.js";
|
||||
import { buildMSTeamsPollCard } from "./polls.js";
|
||||
import { getMSTeamsRuntime } from "./runtime.js";
|
||||
import { resolveMSTeamsSendContext, type MSTeamsProactiveContext } from "./send-context.js";
|
||||
|
||||
export type SendMSTeamsMessageParams = {
|
||||
@@ -93,13 +94,21 @@ export async function sendMessageMSTeams(
|
||||
params: SendMSTeamsMessageParams,
|
||||
): Promise<SendMSTeamsMessageResult> {
|
||||
const { cfg, to, text, mediaUrl } = params;
|
||||
const tableMode = getMSTeamsRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "msteams",
|
||||
});
|
||||
const messageText = getMSTeamsRuntime().channel.text.convertMarkdownTables(
|
||||
text ?? "",
|
||||
tableMode,
|
||||
);
|
||||
const ctx = await resolveMSTeamsSendContext({ cfg, to });
|
||||
const { adapter, appId, conversationId, ref, log, conversationType, tokenProvider, sharePointSiteId } = ctx;
|
||||
|
||||
log.debug("sending proactive message", {
|
||||
conversationId,
|
||||
conversationType,
|
||||
textLength: text.length,
|
||||
textLength: messageText.length,
|
||||
hasMedia: Boolean(mediaUrl),
|
||||
});
|
||||
|
||||
@@ -134,7 +143,7 @@ export async function sendMessageMSTeams(
|
||||
const { activity, uploadId } = prepareFileConsentActivity({
|
||||
media: { buffer: media.buffer, filename: fileName, contentType: media.contentType },
|
||||
conversationId,
|
||||
description: text || undefined,
|
||||
description: messageText || undefined,
|
||||
});
|
||||
|
||||
log.debug("sending file consent card", { uploadId, fileName, size: media.buffer.length });
|
||||
@@ -172,14 +181,14 @@ export async function sendMessageMSTeams(
|
||||
const base64 = media.buffer.toString("base64");
|
||||
const finalMediaUrl = `data:${media.contentType};base64,${base64}`;
|
||||
|
||||
return sendTextWithMedia(ctx, text, finalMediaUrl);
|
||||
return sendTextWithMedia(ctx, messageText, finalMediaUrl);
|
||||
}
|
||||
|
||||
if (isImage && !sharePointSiteId) {
|
||||
// Group chat/channel without SharePoint: send image inline (avoids OneDrive failures)
|
||||
const base64 = media.buffer.toString("base64");
|
||||
const finalMediaUrl = `data:${media.contentType};base64,${base64}`;
|
||||
return sendTextWithMedia(ctx, text, finalMediaUrl);
|
||||
return sendTextWithMedia(ctx, messageText, finalMediaUrl);
|
||||
}
|
||||
|
||||
// Group chat or channel: upload to SharePoint (if siteId configured) or OneDrive
|
||||
@@ -223,7 +232,7 @@ export async function sendMessageMSTeams(
|
||||
const fileCardAttachment = buildTeamsFileInfoCard(driveItem);
|
||||
const activity = {
|
||||
type: "message",
|
||||
text: text || undefined,
|
||||
text: messageText || undefined,
|
||||
attachments: [fileCardAttachment],
|
||||
};
|
||||
|
||||
@@ -264,7 +273,7 @@ export async function sendMessageMSTeams(
|
||||
const fileLink = `📎 [${uploaded.name}](${uploaded.shareUrl})`;
|
||||
const activity = {
|
||||
type: "message",
|
||||
text: text ? `${text}\n\n${fileLink}` : fileLink,
|
||||
text: messageText ? `${messageText}\n\n${fileLink}` : fileLink,
|
||||
};
|
||||
|
||||
const baseRef = buildConversationReference(ref);
|
||||
@@ -290,7 +299,7 @@ export async function sendMessageMSTeams(
|
||||
}
|
||||
|
||||
// No media: send text only
|
||||
return sendTextWithMedia(ctx, text, undefined);
|
||||
return sendTextWithMedia(ctx, messageText, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DmConfigSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
@@ -21,6 +22,7 @@ export const NextcloudTalkAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
baseUrl: z.string().optional(),
|
||||
botSecret: z.string().optional(),
|
||||
botSecretFile: z.string().optional(),
|
||||
|
||||
@@ -71,8 +71,18 @@ export async function sendMessageNextcloudTalk(
|
||||
throw new Error("Message must be non-empty for Nextcloud Talk sends");
|
||||
}
|
||||
|
||||
const tableMode = getNextcloudTalkRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "nextcloud-talk",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const message = getNextcloudTalkRuntime().channel.text.convertMarkdownTables(
|
||||
text.trim(),
|
||||
tableMode,
|
||||
);
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
message: text.trim(),
|
||||
message,
|
||||
};
|
||||
if (opts.replyTo) {
|
||||
body.replyTo = opts.replyTo;
|
||||
|
||||
@@ -133,13 +133,20 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
||||
deliveryMode: "direct",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, accountId }) => {
|
||||
const core = getNostrRuntime();
|
||||
const aid = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const bus = activeBuses.get(aid);
|
||||
if (!bus) {
|
||||
throw new Error(`Nostr bus not running for account ${aid}`);
|
||||
}
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: core.config.loadConfig(),
|
||||
channel: "nostr",
|
||||
accountId: aid,
|
||||
});
|
||||
const message = core.channel.text.convertMarkdownTables(text ?? "", tableMode);
|
||||
const normalizedTo = normalizePubkey(to);
|
||||
await bus.sendDm(normalizedTo, text);
|
||||
await bus.sendDm(normalizedTo, message);
|
||||
return { channel: "nostr", to: normalizedTo };
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MarkdownConfigSchema, buildChannelConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
import { buildChannelConfigSchema } from "clawdbot/plugin-sdk";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
|
||||
@@ -63,6 +63,9 @@ export const NostrConfigSchema = z.object({
|
||||
/** Whether this channel is enabled */
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
/** Markdown formatting overrides (tables). */
|
||||
markdown: MarkdownConfigSchema,
|
||||
|
||||
/** Private key in hex or nsec bech32 format */
|
||||
privateKey: z.string().optional(),
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -5,6 +6,7 @@ const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
const zaloAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
botToken: z.string().optional(),
|
||||
tokenFile: z.string().optional(),
|
||||
webhookUrl: z.string().optional(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import type { ClawdbotConfig, MarkdownTableMode } from "clawdbot/plugin-sdk";
|
||||
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
import {
|
||||
@@ -578,6 +578,12 @@ async function processMessageWithPipeline(params: {
|
||||
runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
|
||||
});
|
||||
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
channel: "zalo",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
|
||||
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
cfg: config,
|
||||
@@ -591,6 +597,7 @@ async function processMessageWithPipeline(params: {
|
||||
core,
|
||||
statusSink,
|
||||
fetcher,
|
||||
tableMode,
|
||||
});
|
||||
},
|
||||
onError: (err, info) => {
|
||||
@@ -608,8 +615,11 @@ async function deliverZaloReply(params: {
|
||||
core: ZaloCoreRuntime;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
fetcher?: ZaloFetch;
|
||||
tableMode?: MarkdownTableMode;
|
||||
}): Promise<void> {
|
||||
const { payload, token, chatId, runtime, core, statusSink, fetcher } = params;
|
||||
const tableMode = params.tableMode ?? "code";
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
|
||||
const mediaList = payload.mediaUrls?.length
|
||||
? payload.mediaUrls
|
||||
@@ -620,7 +630,7 @@ async function deliverZaloReply(params: {
|
||||
if (mediaList.length > 0) {
|
||||
let first = true;
|
||||
for (const mediaUrl of mediaList) {
|
||||
const caption = first ? payload.text : undefined;
|
||||
const caption = first ? text : undefined;
|
||||
first = false;
|
||||
try {
|
||||
await sendPhoto(token, { chat_id: chatId, photo: mediaUrl, caption }, fetcher);
|
||||
@@ -632,8 +642,8 @@ async function deliverZaloReply(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.text) {
|
||||
const chunks = core.channel.text.chunkMarkdownText(payload.text, ZALO_TEXT_LIMIT);
|
||||
if (text) {
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, ZALO_TEXT_LIMIT);
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -10,6 +11,7 @@ const groupConfigSchema = z.object({
|
||||
const zalouserAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
profile: z.string().optional(),
|
||||
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
||||
allowFrom: z.array(allowFromEntry).optional(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import type { ClawdbotConfig, MarkdownTableMode, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import { mergeAllowlist, summarizeMapping } from "clawdbot/plugin-sdk";
|
||||
import { sendMessageZalouser } from "./send.js";
|
||||
import type {
|
||||
@@ -332,6 +332,11 @@ async function processMessage(
|
||||
runtime,
|
||||
core,
|
||||
statusSink,
|
||||
tableMode: core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
channel: "zalouser",
|
||||
accountId: account.accountId,
|
||||
}),
|
||||
});
|
||||
},
|
||||
onError: (err, info) => {
|
||||
@@ -351,8 +356,11 @@ async function deliverZalouserReply(params: {
|
||||
runtime: RuntimeEnv;
|
||||
core: ZalouserCoreRuntime;
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
tableMode?: MarkdownTableMode;
|
||||
}): Promise<void> {
|
||||
const { payload, profile, chatId, isGroup, runtime, core, statusSink } = params;
|
||||
const tableMode = params.tableMode ?? "code";
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
|
||||
const mediaList = payload.mediaUrls?.length
|
||||
? payload.mediaUrls
|
||||
@@ -363,7 +371,7 @@ async function deliverZalouserReply(params: {
|
||||
if (mediaList.length > 0) {
|
||||
let first = true;
|
||||
for (const mediaUrl of mediaList) {
|
||||
const caption = first ? payload.text : undefined;
|
||||
const caption = first ? text : undefined;
|
||||
first = false;
|
||||
try {
|
||||
logVerbose(core, runtime, `Sending media to ${chatId}`);
|
||||
@@ -380,8 +388,8 @@ async function deliverZalouserReply(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.text) {
|
||||
const chunks = core.channel.text.chunkMarkdownText(payload.text, ZALOUSER_TEXT_LIMIT);
|
||||
if (text) {
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, ZALOUSER_TEXT_LIMIT);
|
||||
logVerbose(core, runtime, `Sending ${chunks.length} text chunk(s) to ${chatId}`);
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user