mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(nostr): deliver oversized encrypted replies as ordered chunks (#115816)
Apply the shared, surrogate-safe outbound chunker to the existing Nostr text limit so the real delivery planner produces ordered NIP-04 events. Preserve assistant-visible sanitization and prove short, word, newline, hard-split, and odd-prefix Unicode boundaries. Co-authored-by: liyuanbin <li.yuanbin1@xydigit.com>
This commit is contained in:
committed by
GitHub
parent
0063e46069
commit
85d2d698ff
@@ -126,6 +126,52 @@ describe("nostr outbound cfg threading", () => {
|
||||
expect(sanitizeText({ text, payload: { text } })).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "preserves a short reply as one encrypted message",
|
||||
text: "Hello from Nostr.",
|
||||
expectedChunkCount: 1,
|
||||
joinWith: "",
|
||||
},
|
||||
{
|
||||
name: "splits long replies at word boundaries",
|
||||
text: `${"word ".repeat(1_200)}final`,
|
||||
expectedChunkCount: 2,
|
||||
joinWith: " ",
|
||||
},
|
||||
{
|
||||
name: "preserves newline-delimited reply order",
|
||||
text: `${"line\n".repeat(1_200)}final`,
|
||||
expectedChunkCount: 2,
|
||||
joinWith: "\n",
|
||||
},
|
||||
{
|
||||
name: "hard-splits an uninterrupted oversized reply",
|
||||
text: "x".repeat(8_001),
|
||||
expectedChunkCount: 3,
|
||||
joinWith: "",
|
||||
},
|
||||
{
|
||||
name: "preserves Unicode when an odd prefix shifts the chunk boundary",
|
||||
text: `a${"😀".repeat(2_500)}`,
|
||||
expectedChunkCount: 2,
|
||||
joinWith: "",
|
||||
},
|
||||
])("$name", ({ text, expectedChunkCount, joinWith }) => {
|
||||
const outbound = nostrPlugin.outbound;
|
||||
const textChunkLimit = outbound?.textChunkLimit;
|
||||
expect(textChunkLimit).toBe(4_000);
|
||||
expect(outbound?.chunker).toBeTypeOf("function");
|
||||
if (!outbound?.chunker || textChunkLimit === undefined) {
|
||||
throw new Error("Expected Nostr outbound text chunking");
|
||||
}
|
||||
|
||||
const chunks = outbound.chunker(text, textChunkLimit);
|
||||
expect(chunks).toHaveLength(expectedChunkCount);
|
||||
expect(chunks.every((chunk) => chunk.length <= textChunkLimit)).toBe(true);
|
||||
expect(chunks.join(joinWith)).toBe(text);
|
||||
});
|
||||
|
||||
it("converts tables before projecting markdown to Nostr plain text", async () => {
|
||||
const { resolveMarkdownTableMode, convertMarkdownTables } = installOutboundRuntime(
|
||||
vi.fn((text: string) => (text === "***" ? text : "**Table:** [docs](https://example.com)")),
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
|
||||
import {
|
||||
chunkTextForOutbound,
|
||||
sanitizeAssistantVisibleText,
|
||||
stripMarkdown,
|
||||
} from "openclaw/plugin-sdk/text-chunking";
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "./channel-api.js";
|
||||
import type { MetricEvent, MetricsSnapshot } from "./metrics.js";
|
||||
import { startNostrBus, type NostrBusHandle } from "./nostr-bus.js";
|
||||
@@ -23,7 +27,7 @@ type NostrGatewayStart = NonNullable<
|
||||
>;
|
||||
type NostrOutboundAdapter = Pick<
|
||||
ChannelOutboundAdapter,
|
||||
"deliveryCapabilities" | "deliveryMode" | "textChunkLimit" | "sendText"
|
||||
"chunker" | "deliveryCapabilities" | "deliveryMode" | "textChunkLimit" | "sendText"
|
||||
> & {
|
||||
sendText: NonNullable<ChannelOutboundAdapter["sendText"]>;
|
||||
sanitizeText: NonNullable<ChannelOutboundAdapter["sanitizeText"]>;
|
||||
@@ -316,6 +320,9 @@ export const nostrPairingTextAdapter = {
|
||||
export const nostrOutboundAdapter: NostrOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
textChunkLimit: 4000,
|
||||
// The outbound planner ignores textChunkLimit unless the adapter also
|
||||
// supplies its chunker, causing oversized encrypted events to be rejected.
|
||||
chunker: chunkTextForOutbound,
|
||||
sanitizeText: ({ text }) => sanitizeAssistantVisibleText(text),
|
||||
deliveryCapabilities: {
|
||||
durableFinal: {
|
||||
|
||||
Reference in New Issue
Block a user