feat: configurable outbound text chunk limits
This commit is contained in:
@@ -55,6 +55,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
|
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
|
||||||
- Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.
|
- Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.
|
||||||
|
- Messages: make outbound text chunk limits configurable (defaults remain 4000/Discord 2000).
|
||||||
- CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.
|
- CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.
|
||||||
- Control UI: accept a `?token=` URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured.
|
- Control UI: accept a `?token=` URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured.
|
||||||
- Agent prompt: remove hardcoded user name in system prompt example.
|
- Agent prompt: remove hardcoded user name in system prompt example.
|
||||||
|
|||||||
@@ -282,7 +282,17 @@ Controls inbound/outbound prefixes and timestamps.
|
|||||||
messages: {
|
messages: {
|
||||||
messagePrefix: "[clawdis]",
|
messagePrefix: "[clawdis]",
|
||||||
responsePrefix: "🦞",
|
responsePrefix: "🦞",
|
||||||
timestampPrefix: "Europe/London"
|
timestampPrefix: "Europe/London",
|
||||||
|
// outbound chunk size (chars); defaults vary by surface (e.g. 4000, Discord 2000)
|
||||||
|
textChunkLimit: 4000,
|
||||||
|
// optional per-surface overrides
|
||||||
|
textChunkLimitBySurface: {
|
||||||
|
whatsapp: 4000,
|
||||||
|
telegram: 4000,
|
||||||
|
signal: 4000,
|
||||||
|
imessage: 4000,
|
||||||
|
discord: 2000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { chunkText } from "./chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "./chunk.js";
|
||||||
|
|
||||||
describe("chunkText", () => {
|
describe("chunkText", () => {
|
||||||
it("keeps multi-line text in one chunk when under limit", () => {
|
it("keeps multi-line text in one chunk when under limit", () => {
|
||||||
@@ -45,3 +45,30 @@ describe("chunkText", () => {
|
|||||||
expect(chunks).toEqual(["Supercalif", "ragilistic", "expialidoc", "ious"]);
|
expect(chunks).toEqual(["Supercalif", "ragilistic", "expialidoc", "ious"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveTextChunkLimit", () => {
|
||||||
|
it("uses per-surface defaults", () => {
|
||||||
|
expect(resolveTextChunkLimit(undefined, "whatsapp")).toBe(4000);
|
||||||
|
expect(resolveTextChunkLimit(undefined, "telegram")).toBe(4000);
|
||||||
|
expect(resolveTextChunkLimit(undefined, "signal")).toBe(4000);
|
||||||
|
expect(resolveTextChunkLimit(undefined, "imessage")).toBe(4000);
|
||||||
|
expect(resolveTextChunkLimit(undefined, "discord")).toBe(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports a global override", () => {
|
||||||
|
const cfg = { messages: { textChunkLimit: 1234 } };
|
||||||
|
expect(resolveTextChunkLimit(cfg, "whatsapp")).toBe(1234);
|
||||||
|
expect(resolveTextChunkLimit(cfg, "discord")).toBe(1234);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers per-surface overrides over global", () => {
|
||||||
|
const cfg = {
|
||||||
|
messages: {
|
||||||
|
textChunkLimit: 1234,
|
||||||
|
textChunkLimitBySurface: { discord: 111 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(resolveTextChunkLimit(cfg, "discord")).toBe(111);
|
||||||
|
expect(resolveTextChunkLimit(cfg, "telegram")).toBe(1234);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,43 @@
|
|||||||
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
|
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
|
||||||
// the chunk so messages are only split when they truly exceed the limit.
|
// the chunk so messages are only split when they truly exceed the limit.
|
||||||
|
|
||||||
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
|
|
||||||
|
export type TextChunkSurface =
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "signal"
|
||||||
|
| "imessage"
|
||||||
|
| "webchat";
|
||||||
|
|
||||||
|
const DEFAULT_CHUNK_LIMIT_BY_SURFACE: Record<TextChunkSurface, number> = {
|
||||||
|
whatsapp: 4000,
|
||||||
|
telegram: 4000,
|
||||||
|
discord: 2000,
|
||||||
|
signal: 4000,
|
||||||
|
imessage: 4000,
|
||||||
|
webchat: 4000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveTextChunkLimit(
|
||||||
|
cfg: Pick<ClawdisConfig, "messages"> | undefined,
|
||||||
|
surface?: TextChunkSurface,
|
||||||
|
): number {
|
||||||
|
const surfaceOverride = surface
|
||||||
|
? cfg?.messages?.textChunkLimitBySurface?.[surface]
|
||||||
|
: undefined;
|
||||||
|
if (typeof surfaceOverride === "number" && surfaceOverride > 0) {
|
||||||
|
return surfaceOverride;
|
||||||
|
}
|
||||||
|
const globalOverride = cfg?.messages?.textChunkLimit;
|
||||||
|
if (typeof globalOverride === "number" && globalOverride > 0) {
|
||||||
|
return globalOverride;
|
||||||
|
}
|
||||||
|
if (surface) return DEFAULT_CHUNK_LIMIT_BY_SURFACE[surface];
|
||||||
|
return 4000;
|
||||||
|
}
|
||||||
|
|
||||||
export function chunkText(text: string, limit: number): string[] {
|
export function chunkText(text: string, limit: number): string[] {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
if (limit <= 0) return [text];
|
if (limit <= 0) return [text];
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
ensureAgentWorkspace,
|
ensureAgentWorkspace,
|
||||||
} from "../agents/workspace.js";
|
} from "../agents/workspace.js";
|
||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import type { MsgContext } from "../auto-reply/templating.js";
|
import type { MsgContext } from "../auto-reply/templating.js";
|
||||||
import {
|
import {
|
||||||
normalizeThinkLevel,
|
normalizeThinkLevel,
|
||||||
@@ -524,6 +524,15 @@ export async function agentCommand(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deliveryTextLimit =
|
||||||
|
deliveryProvider === "whatsapp" ||
|
||||||
|
deliveryProvider === "telegram" ||
|
||||||
|
deliveryProvider === "discord" ||
|
||||||
|
deliveryProvider === "signal" ||
|
||||||
|
deliveryProvider === "imessage"
|
||||||
|
? resolveTextChunkLimit(cfg, deliveryProvider)
|
||||||
|
: resolveTextChunkLimit(cfg, "whatsapp");
|
||||||
|
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
const mediaList =
|
const mediaList =
|
||||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
@@ -564,7 +573,7 @@ export async function agentCommand(
|
|||||||
if (deliveryProvider === "telegram" && telegramTarget) {
|
if (deliveryProvider === "telegram" && telegramTarget) {
|
||||||
try {
|
try {
|
||||||
if (media.length === 0) {
|
if (media.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, deliveryTextLimit)) {
|
||||||
await deps.sendMessageTelegram(telegramTarget, chunk, {
|
await deps.sendMessageTelegram(telegramTarget, chunk, {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
token: telegramToken || undefined,
|
token: telegramToken || undefined,
|
||||||
@@ -645,7 +654,7 @@ export async function agentCommand(
|
|||||||
if (deliveryProvider === "imessage" && imessageTarget) {
|
if (deliveryProvider === "imessage" && imessageTarget) {
|
||||||
try {
|
try {
|
||||||
if (media.length === 0) {
|
if (media.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, deliveryTextLimit)) {
|
||||||
await deps.sendMessageIMessage(imessageTarget, chunk, {
|
await deps.sendMessageIMessage(imessageTarget, chunk, {
|
||||||
maxBytes: cfg.imessage?.mediaMaxMb
|
maxBytes: cfg.imessage?.mediaMaxMb
|
||||||
? cfg.imessage.mediaMaxMb * 1024 * 1024
|
? cfg.imessage.mediaMaxMb * 1024 * 1024
|
||||||
|
|||||||
@@ -314,6 +314,15 @@ export type MessagesConfig = {
|
|||||||
messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdis]" if no allowFrom, else "")
|
messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdis]" if no allowFrom, else "")
|
||||||
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
|
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
|
||||||
timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC)
|
timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC)
|
||||||
|
/** Outbound text chunk size (chars). Default varies by provider (e.g. 4000, Discord 2000). */
|
||||||
|
textChunkLimit?: number;
|
||||||
|
/** Optional per-surface chunk overrides. */
|
||||||
|
textChunkLimitBySurface?: Partial<
|
||||||
|
Record<
|
||||||
|
"whatsapp" | "telegram" | "discord" | "signal" | "imessage" | "webchat",
|
||||||
|
number
|
||||||
|
>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BridgeBindMode = "auto" | "lan" | "tailnet" | "loopback";
|
export type BridgeBindMode = "auto" | "lan" | "tailnet" | "loopback";
|
||||||
@@ -708,6 +717,17 @@ const MessagesSchema = z
|
|||||||
messagePrefix: z.string().optional(),
|
messagePrefix: z.string().optional(),
|
||||||
responsePrefix: z.string().optional(),
|
responsePrefix: z.string().optional(),
|
||||||
timestampPrefix: z.union([z.boolean(), z.string()]).optional(),
|
timestampPrefix: z.union([z.boolean(), z.string()]).optional(),
|
||||||
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
textChunkLimitBySurface: z
|
||||||
|
.object({
|
||||||
|
whatsapp: z.number().int().positive().optional(),
|
||||||
|
telegram: z.number().int().positive().optional(),
|
||||||
|
discord: z.number().int().positive().optional(),
|
||||||
|
signal: z.number().int().positive().optional(),
|
||||||
|
imessage: z.number().int().positive().optional(),
|
||||||
|
webchat: z.number().int().positive().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
ensureAgentWorkspace,
|
ensureAgentWorkspace,
|
||||||
} from "../agents/workspace.js";
|
} from "../agents/workspace.js";
|
||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { normalizeThinkLevel } from "../auto-reply/thinking.js";
|
import { normalizeThinkLevel } from "../auto-reply/thinking.js";
|
||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
@@ -357,12 +357,13 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const chatId = resolvedDelivery.to;
|
const chatId = resolvedDelivery.to;
|
||||||
|
const textLimit = resolveTextChunkLimit(params.cfg, "telegram");
|
||||||
try {
|
try {
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
const mediaList =
|
const mediaList =
|
||||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkText(payload.text ?? "", 4000)) {
|
for (const chunk of chunkText(payload.text ?? "", textLimit)) {
|
||||||
await params.deps.sendMessageTelegram(chatId, chunk, {
|
await params.deps.sendMessageTelegram(chatId, chunk, {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
token: telegramToken || undefined,
|
token: telegramToken || undefined,
|
||||||
@@ -444,12 +445,13 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const to = resolvedDelivery.to;
|
const to = resolvedDelivery.to;
|
||||||
|
const textLimit = resolveTextChunkLimit(params.cfg, "signal");
|
||||||
try {
|
try {
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
const mediaList =
|
const mediaList =
|
||||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkText(payload.text ?? "", 4000)) {
|
for (const chunk of chunkText(payload.text ?? "", textLimit)) {
|
||||||
await params.deps.sendMessageSignal(to, chunk);
|
await params.deps.sendMessageSignal(to, chunk);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -482,12 +484,13 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const to = resolvedDelivery.to;
|
const to = resolvedDelivery.to;
|
||||||
|
const textLimit = resolveTextChunkLimit(params.cfg, "imessage");
|
||||||
try {
|
try {
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
const mediaList =
|
const mediaList =
|
||||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkText(payload.text ?? "", 4000)) {
|
for (const chunk of chunkText(payload.text ?? "", textLimit)) {
|
||||||
await params.deps.sendMessageIMessage(to, chunk);
|
await params.deps.sendMessageIMessage(to, chunk);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
@@ -111,15 +111,16 @@ async function deliverReplies(params: {
|
|||||||
client: Awaited<ReturnType<typeof createIMessageRpcClient>>;
|
client: Awaited<ReturnType<typeof createIMessageRpcClient>>;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
maxBytes: number;
|
maxBytes: number;
|
||||||
|
textLimit: number;
|
||||||
}) {
|
}) {
|
||||||
const { replies, target, client, runtime, maxBytes } = params;
|
const { replies, target, client, runtime, maxBytes, textLimit } = params;
|
||||||
for (const payload of replies) {
|
for (const payload of replies) {
|
||||||
const mediaList =
|
const mediaList =
|
||||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
const text = payload.text ?? "";
|
const text = payload.text ?? "";
|
||||||
if (!text && mediaList.length === 0) continue;
|
if (!text && mediaList.length === 0) continue;
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, textLimit)) {
|
||||||
await sendMessageIMessage(target, chunk, { maxBytes, client });
|
await sendMessageIMessage(target, chunk, { maxBytes, client });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -143,6 +144,7 @@ export async function monitorIMessageProvider(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const runtime = resolveRuntime(opts);
|
const runtime = resolveRuntime(opts);
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
const textLimit = resolveTextChunkLimit(cfg, "imessage");
|
||||||
const allowFrom = resolveAllowFrom(opts);
|
const allowFrom = resolveAllowFrom(opts);
|
||||||
const mentionRegexes = resolveMentionRegexes(cfg);
|
const mentionRegexes = resolveMentionRegexes(cfg);
|
||||||
const includeAttachments =
|
const includeAttachments =
|
||||||
@@ -274,6 +276,7 @@ export async function monitorIMessageProvider(
|
|||||||
client,
|
client,
|
||||||
runtime,
|
runtime,
|
||||||
maxBytes: mediaMaxBytes,
|
maxBytes: mediaMaxBytes,
|
||||||
|
textLimit,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -302,6 +305,7 @@ export async function monitorIMessageProvider(
|
|||||||
client,
|
client,
|
||||||
runtime,
|
runtime,
|
||||||
maxBytes: mediaMaxBytes,
|
maxBytes: mediaMaxBytes,
|
||||||
|
textLimit,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import {
|
import {
|
||||||
HEARTBEAT_PROMPT,
|
HEARTBEAT_PROMPT,
|
||||||
stripHeartbeatToken,
|
stripHeartbeatToken,
|
||||||
@@ -292,6 +292,7 @@ async function deliverHeartbeatReply(params: {
|
|||||||
to: string;
|
to: string;
|
||||||
text: string;
|
text: string;
|
||||||
mediaUrls: string[];
|
mediaUrls: string[];
|
||||||
|
textLimit: number;
|
||||||
deps: Required<
|
deps: Required<
|
||||||
Pick<
|
Pick<
|
||||||
HeartbeatDeps,
|
HeartbeatDeps,
|
||||||
@@ -303,10 +304,10 @@ async function deliverHeartbeatReply(params: {
|
|||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
}) {
|
}) {
|
||||||
const { channel, to, text, mediaUrls, deps } = params;
|
const { channel, to, text, mediaUrls, deps, textLimit } = params;
|
||||||
if (channel === "whatsapp") {
|
if (channel === "whatsapp") {
|
||||||
if (mediaUrls.length === 0) {
|
if (mediaUrls.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, textLimit)) {
|
||||||
await deps.sendWhatsApp(to, chunk, { verbose: false });
|
await deps.sendWhatsApp(to, chunk, { verbose: false });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -322,7 +323,7 @@ async function deliverHeartbeatReply(params: {
|
|||||||
|
|
||||||
if (channel === "signal") {
|
if (channel === "signal") {
|
||||||
if (mediaUrls.length === 0) {
|
if (mediaUrls.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, textLimit)) {
|
||||||
await deps.sendSignal(to, chunk);
|
await deps.sendSignal(to, chunk);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -338,7 +339,7 @@ async function deliverHeartbeatReply(params: {
|
|||||||
|
|
||||||
if (channel === "imessage") {
|
if (channel === "imessage") {
|
||||||
if (mediaUrls.length === 0) {
|
if (mediaUrls.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, textLimit)) {
|
||||||
await deps.sendIMessage(to, chunk);
|
await deps.sendIMessage(to, chunk);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -354,7 +355,7 @@ async function deliverHeartbeatReply(params: {
|
|||||||
|
|
||||||
if (channel === "telegram") {
|
if (channel === "telegram") {
|
||||||
if (mediaUrls.length === 0) {
|
if (mediaUrls.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, textLimit)) {
|
||||||
await deps.sendTelegram(to, chunk, { verbose: false });
|
await deps.sendTelegram(to, chunk, { verbose: false });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -500,11 +501,13 @@ export async function runHeartbeatOnce(opts: {
|
|||||||
sendSignal: opts.deps?.sendSignal ?? sendMessageSignal,
|
sendSignal: opts.deps?.sendSignal ?? sendMessageSignal,
|
||||||
sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage,
|
sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage,
|
||||||
};
|
};
|
||||||
|
const textLimit = resolveTextChunkLimit(cfg, delivery.channel);
|
||||||
await deliverHeartbeatReply({
|
await deliverHeartbeatReply({
|
||||||
channel: delivery.channel,
|
channel: delivery.channel,
|
||||||
to: delivery.to,
|
to: delivery.to,
|
||||||
text: normalized.text,
|
text: normalized.text,
|
||||||
mediaUrls,
|
mediaUrls,
|
||||||
|
textLimit,
|
||||||
deps,
|
deps,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
@@ -178,15 +178,17 @@ async function deliverReplies(params: {
|
|||||||
account?: string;
|
account?: string;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
maxBytes: number;
|
maxBytes: number;
|
||||||
|
textLimit: number;
|
||||||
}) {
|
}) {
|
||||||
const { replies, target, baseUrl, account, runtime, maxBytes } = params;
|
const { replies, target, baseUrl, account, runtime, maxBytes, textLimit } =
|
||||||
|
params;
|
||||||
for (const payload of replies) {
|
for (const payload of replies) {
|
||||||
const mediaList =
|
const mediaList =
|
||||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
const text = payload.text ?? "";
|
const text = payload.text ?? "";
|
||||||
if (!text && mediaList.length === 0) continue;
|
if (!text && mediaList.length === 0) continue;
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkText(text, 4000)) {
|
for (const chunk of chunkText(text, textLimit)) {
|
||||||
await sendMessageSignal(target, chunk, {
|
await sendMessageSignal(target, chunk, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
account,
|
account,
|
||||||
@@ -215,6 +217,7 @@ export async function monitorSignalProvider(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const runtime = resolveRuntime(opts);
|
const runtime = resolveRuntime(opts);
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
const textLimit = resolveTextChunkLimit(cfg, "signal");
|
||||||
const baseUrl = resolveBaseUrl(opts);
|
const baseUrl = resolveBaseUrl(opts);
|
||||||
const account = resolveAccount(opts);
|
const account = resolveAccount(opts);
|
||||||
const allowFrom = resolveAllowFrom(opts);
|
const allowFrom = resolveAllowFrom(opts);
|
||||||
@@ -391,6 +394,7 @@ export async function monitorSignalProvider(
|
|||||||
account,
|
account,
|
||||||
runtime,
|
runtime,
|
||||||
maxBytes: mediaMaxBytes,
|
maxBytes: mediaMaxBytes,
|
||||||
|
textLimit,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -420,6 +424,7 @@ export async function monitorSignalProvider(
|
|||||||
account,
|
account,
|
||||||
runtime,
|
runtime,
|
||||||
maxBytes: mediaMaxBytes,
|
maxBytes: mediaMaxBytes,
|
||||||
|
textLimit,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { apiThrottler } from "@grammyjs/transformer-throttler";
|
|||||||
import type { ApiClientOptions, Message } from "grammy";
|
import type { ApiClientOptions, Message } from "grammy";
|
||||||
import { Bot, InputFile, webhookCallback } from "grammy";
|
import { Bot, InputFile, webhookCallback } from "grammy";
|
||||||
|
|
||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
@@ -60,6 +60,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
bot.api.config.use(apiThrottler());
|
bot.api.config.use(apiThrottler());
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
const textLimit = resolveTextChunkLimit(cfg, "telegram");
|
||||||
const allowFrom = opts.allowFrom ?? cfg.telegram?.allowFrom;
|
const allowFrom = opts.allowFrom ?? cfg.telegram?.allowFrom;
|
||||||
const replyToMode = opts.replyToMode ?? cfg.telegram?.replyToMode ?? "off";
|
const replyToMode = opts.replyToMode ?? cfg.telegram?.replyToMode ?? "off";
|
||||||
const mediaMaxBytes =
|
const mediaMaxBytes =
|
||||||
@@ -245,6 +246,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
runtime,
|
runtime,
|
||||||
bot,
|
bot,
|
||||||
replyToMode,
|
replyToMode,
|
||||||
|
textLimit,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
runtime.error?.(danger(`handler failed: ${String(err)}`));
|
runtime.error?.(danger(`handler failed: ${String(err)}`));
|
||||||
@@ -268,8 +270,9 @@ async function deliverReplies(params: {
|
|||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
bot: Bot;
|
bot: Bot;
|
||||||
replyToMode: ReplyToMode;
|
replyToMode: ReplyToMode;
|
||||||
|
textLimit: number;
|
||||||
}) {
|
}) {
|
||||||
const { replies, chatId, runtime, bot, replyToMode } = params;
|
const { replies, chatId, runtime, bot, replyToMode, textLimit } = params;
|
||||||
let hasReplied = false;
|
let hasReplied = false;
|
||||||
for (const reply of replies) {
|
for (const reply of replies) {
|
||||||
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
|
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
|
||||||
@@ -286,7 +289,7 @@ async function deliverReplies(params: {
|
|||||||
? [reply.mediaUrl]
|
? [reply.mediaUrl]
|
||||||
: [];
|
: [];
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkText(reply.text || "", 4000)) {
|
for (const chunk of chunkText(reply.text || "", textLimit)) {
|
||||||
await sendTelegramText(bot, chatId, chunk, runtime, {
|
await sendTelegramText(bot, chatId, chunk, runtime, {
|
||||||
replyToMessageId:
|
replyToMessageId:
|
||||||
replyToId && (replyToMode === "all" || !hasReplied)
|
replyToId && (replyToMode === "all" || !hasReplied)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||||
import {
|
import {
|
||||||
normalizeGroupActivation,
|
normalizeGroupActivation,
|
||||||
@@ -41,7 +41,6 @@ import {
|
|||||||
} from "./reconnect.js";
|
} from "./reconnect.js";
|
||||||
import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js";
|
import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js";
|
||||||
|
|
||||||
const WEB_TEXT_LIMIT = 4000;
|
|
||||||
const DEFAULT_GROUP_HISTORY_LIMIT = 50;
|
const DEFAULT_GROUP_HISTORY_LIMIT = 50;
|
||||||
const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp");
|
const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp");
|
||||||
const whatsappInboundLog = whatsappLog.child("inbound");
|
const whatsappInboundLog = whatsappLog.child("inbound");
|
||||||
@@ -502,6 +501,7 @@ async function deliverWebReply(params: {
|
|||||||
replyResult: ReplyPayload;
|
replyResult: ReplyPayload;
|
||||||
msg: WebInboundMsg;
|
msg: WebInboundMsg;
|
||||||
maxMediaBytes: number;
|
maxMediaBytes: number;
|
||||||
|
textLimit: number;
|
||||||
replyLogger: ReturnType<typeof getChildLogger>;
|
replyLogger: ReturnType<typeof getChildLogger>;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
skipLog?: boolean;
|
skipLog?: boolean;
|
||||||
@@ -510,12 +510,13 @@ async function deliverWebReply(params: {
|
|||||||
replyResult,
|
replyResult,
|
||||||
msg,
|
msg,
|
||||||
maxMediaBytes,
|
maxMediaBytes,
|
||||||
|
textLimit,
|
||||||
replyLogger,
|
replyLogger,
|
||||||
connectionId,
|
connectionId,
|
||||||
skipLog,
|
skipLog,
|
||||||
} = params;
|
} = params;
|
||||||
const replyStarted = Date.now();
|
const replyStarted = Date.now();
|
||||||
const textChunks = chunkText(replyResult.text || "", WEB_TEXT_LIMIT);
|
const textChunks = chunkText(replyResult.text || "", textLimit);
|
||||||
const mediaList = replyResult.mediaUrls?.length
|
const mediaList = replyResult.mediaUrls?.length
|
||||||
? replyResult.mediaUrls
|
? replyResult.mediaUrls
|
||||||
: replyResult.mediaUrl
|
: replyResult.mediaUrl
|
||||||
@@ -1050,6 +1051,7 @@ export async function monitorWebProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const responsePrefix = cfg.messages?.responsePrefix;
|
const responsePrefix = cfg.messages?.responsePrefix;
|
||||||
|
const textLimit = resolveTextChunkLimit(cfg, "whatsapp");
|
||||||
let didLogHeartbeatStrip = false;
|
let didLogHeartbeatStrip = false;
|
||||||
let didSendReply = false;
|
let didSendReply = false;
|
||||||
let toolSendChain: Promise<void> = Promise.resolve();
|
let toolSendChain: Promise<void> = Promise.resolve();
|
||||||
@@ -1091,6 +1093,7 @@ export async function monitorWebProvider(
|
|||||||
replyResult: toolPayload,
|
replyResult: toolPayload,
|
||||||
msg,
|
msg,
|
||||||
maxMediaBytes,
|
maxMediaBytes,
|
||||||
|
textLimit,
|
||||||
replyLogger,
|
replyLogger,
|
||||||
connectionId,
|
connectionId,
|
||||||
skipLog: true,
|
skipLog: true,
|
||||||
@@ -1134,6 +1137,7 @@ export async function monitorWebProvider(
|
|||||||
replyResult: blockPayload,
|
replyResult: blockPayload,
|
||||||
msg,
|
msg,
|
||||||
maxMediaBytes,
|
maxMediaBytes,
|
||||||
|
textLimit,
|
||||||
replyLogger,
|
replyLogger,
|
||||||
connectionId,
|
connectionId,
|
||||||
skipLog: true,
|
skipLog: true,
|
||||||
@@ -1238,6 +1242,7 @@ export async function monitorWebProvider(
|
|||||||
replyResult: replyPayload,
|
replyResult: replyPayload,
|
||||||
msg,
|
msg,
|
||||||
maxMediaBytes,
|
maxMediaBytes,
|
||||||
|
textLimit,
|
||||||
replyLogger,
|
replyLogger,
|
||||||
connectionId,
|
connectionId,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user