惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
D
Docker
酷 壳 – CoolShell
酷 壳 – CoolShell
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
美团技术团队
量子位
M
MIT News - Artificial intelligence
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
博客园 - 叶小钗
博客园 - 三生石上(FineUI控件)
腾讯CDC
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
小众软件
小众软件
博客园 - 司徒正美
罗磊的独立博客
云风的 BLOG
云风的 BLOG
B
Blog RSS Feed
博客园 - 聂微东

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
refactor(outbound): plan text and media sends · openclaw/...
steipete · 2026-04-25 · via Recent Commits to openclaw:main

@@ -1,10 +1,4 @@

1-

import { sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";

2-

import {

3-

chunkByParagraph,

4-

chunkMarkdownTextWithMode,

5-

resolveChunkMode,

6-

resolveTextChunkLimit,

7-

} from "../../auto-reply/chunk.js";

1+

import { resolveChunkMode, resolveTextChunkLimit } from "../../auto-reply/chunk.js";

82

import type { ReplyPayload } from "../../auto-reply/types.js";

93

import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";

104

import type {

@@ -45,6 +39,11 @@ import {

4539

} from "./delivery-queue.js";

4640

import type { OutboundDeliveryFormattingOptions } from "./formatting.js";

4741

import type { OutboundIdentity } from "./identity.js";

42+

import {

43+

planOutboundMediaMessageUnits,

44+

planOutboundTextMessageUnits,

45+

type OutboundMessageSendOverrides,

46+

} from "./message-plan.js";

4847

import type { DeliveryMirror } from "./mirror.js";

4948

import {

5049

createOutboundPayloadPlan,

@@ -82,14 +81,8 @@ async function loadChannelBootstrapRuntime() {

8281

return await channelBootstrapRuntimePromise;

8382

}

848385-

type Chunker = (

86-

text: string,

87-

limit: number,

88-

ctx?: { formatting?: OutboundDeliveryFormattingOptions },

89-

) => string[];

90-9184

type ChannelHandler = {

92-

chunker: Chunker | null;

85+

chunker: ChannelOutboundAdapter["chunker"] | null;

9386

chunkerMode?: "text" | "markdown";

9487

textChunkLimit?: number;

9588

supportsMedia: boolean;

@@ -111,45 +104,25 @@ type ChannelHandler = {

111104

resolveEffectiveTextChunkLimit?: (fallbackLimit?: number) => number | undefined;

112105

sendPayload?: (

113106

payload: ReplyPayload,

114-

overrides?: {

115-

replyToId?: string | null;

116-

threadId?: string | number | null;

117-

audioAsVoice?: boolean;

118-

},

107+

overrides?: OutboundMessageSendOverrides,

119108

) => Promise<OutboundDeliveryResult>;

120109

sendFormattedText?: (

121110

text: string,

122-

overrides?: {

123-

replyToId?: string | null;

124-

threadId?: string | number | null;

125-

audioAsVoice?: boolean;

126-

},

111+

overrides?: OutboundMessageSendOverrides,

127112

) => Promise<OutboundDeliveryResult[]>;

128113

sendFormattedMedia?: (

129114

caption: string,

130115

mediaUrl: string,

131-

overrides?: {

132-

replyToId?: string | null;

133-

threadId?: string | number | null;

134-

audioAsVoice?: boolean;

135-

},

116+

overrides?: OutboundMessageSendOverrides,

136117

) => Promise<OutboundDeliveryResult>;

137118

sendText: (

138119

text: string,

139-

overrides?: {

140-

replyToId?: string | null;

141-

threadId?: string | number | null;

142-

audioAsVoice?: boolean;

143-

},

120+

overrides?: OutboundMessageSendOverrides,

144121

) => Promise<OutboundDeliveryResult>;

145122

sendMedia: (

146123

caption: string,

147124

mediaUrl: string,

148-

overrides?: {

149-

replyToId?: string | null;

150-

threadId?: string | number | null;

151-

audioAsVoice?: boolean;

152-

},

125+

overrides?: OutboundMessageSendOverrides,

153126

) => Promise<OutboundDeliveryResult>;

154127

};

155128

@@ -203,11 +176,16 @@ function createPluginHandler(

203176

const chunkerMode = outbound.chunkerMode;

204177

const resolveCtx = (overrides?: {

205178

replyToId?: string | null;

179+

replyToIdSource?: "explicit" | "implicit";

206180

threadId?: string | number | null;

207181

audioAsVoice?: boolean;

208182

}): Omit<ChannelOutboundContext, "text" | "mediaUrl"> => ({

209183

...baseCtx,

210184

replyToId: overrides && "replyToId" in overrides ? overrides.replyToId : baseCtx.replyToId,

185+

replyToIdSource:

186+

overrides && "replyToIdSource" in overrides

187+

? overrides.replyToIdSource

188+

: baseCtx.replyToIdSource,

211189

threadId: overrides && "threadId" in overrides ? overrides.threadId : baseCtx.threadId,

212190

audioAsVoice: overrides?.audioAsVoice,

213191

});

@@ -841,55 +819,27 @@ async function deliverOutboundPayloadsCore(

841819

replyToId: params.replyToId,

842820

replyToMode: params.replyToMode,

843821

});

844-

const chunkTextForDelivery = (text: string, limit: number): string[] =>

845-

params.formatting

846-

? handler.chunker!(text, limit, { formatting: params.formatting })

847-

: handler.chunker!(text, limit);

848822849-

const sendTextChunks = async (

850-

text: string,

851-

overrides?: {

852-

replyToId?: string | null;

853-

replyToIdSource?: "explicit" | "implicit";

854-

threadId?: string | number | null;

855-

audioAsVoice?: boolean;

856-

},

857-

) => {

858-

const consumeReplyTo = <T extends NonNullable<typeof overrides>>(value: T): T =>

859-

applyReplyToConsumption(value, {

860-

consumeImplicitReply: value.replyToIdSource === "implicit",

861-

});

862-

throwIfAborted(abortSignal);

863-

if (!handler.chunker || textLimit === undefined) {

864-

results.push(await handler.sendText(text, consumeReplyTo(overrides ?? {})));

865-

return;

866-

}

867-

if (chunkMode === "newline") {

868-

const mode = handler.chunkerMode ?? "text";

869-

const blockChunks =

870-

mode === "markdown"

871-

? chunkMarkdownTextWithMode(text, textLimit, "newline")

872-

: chunkByParagraph(text, textLimit);

873-874-

if (!blockChunks.length && text) {

875-

blockChunks.push(text);

876-

}

877-

for (const blockChunk of blockChunks) {

878-

const chunks = chunkTextForDelivery(blockChunk, textLimit);

879-

if (!chunks.length && blockChunk) {

880-

chunks.push(blockChunk);

881-

}

882-

for (const chunk of chunks) {

883-

throwIfAborted(abortSignal);

884-

results.push(await handler.sendText(chunk, consumeReplyTo(overrides ?? {})));

885-

}

823+

const sendTextChunks = async (text: string, overrides: OutboundMessageSendOverrides = {}) => {

824+

const units = planOutboundTextMessageUnits({

825+

text,

826+

overrides,

827+

chunker: handler.chunker,

828+

chunkerMode: handler.chunkerMode,

829+

textLimit,

830+

chunkMode,

831+

formatting: params.formatting,

832+

consumeReplyTo: (value) =>

833+

applyReplyToConsumption(value, {

834+

consumeImplicitReply: value.replyToIdSource === "implicit",

835+

}),

836+

});

837+

for (const unit of units) {

838+

if (unit.kind !== "text") {

839+

continue;

886840

}

887-

return;

888-

}

889-

const chunks = chunkTextForDelivery(text, textLimit);

890-

for (const chunk of chunks) {

891841

throwIfAborted(abortSignal);

892-

results.push(await handler.sendText(chunk, consumeReplyTo(overrides ?? {})));

842+

results.push(await handler.sendText(unit.text, unit.overrides));

893843

}

894844

};

895845

const normalizedPayloads = normalizePayloadsForChannelDelivery(outboundPayloadPlan, handler);

@@ -951,14 +901,16 @@ async function deliverOutboundPayloadsCore(

951901952902

params.onPayload?.(payloadSummary);

953903

const replyToResolution = resolveCurrentReplyTo(effectivePayload);

954-

const sendOverrides = {

904+

const sendOverrides: OutboundMessageSendOverrides = {

955905

replyToId: replyToResolution.replyToId,

956906

replyToIdSource: replyToResolution.source,

957-

threadId: params.threadId ?? undefined,

958-

audioAsVoice: effectivePayload.audioAsVoice === true ? true : undefined,

959-

forceDocument: params.forceDocument,

907+

...(params.threadId !== undefined ? { threadId: params.threadId } : {}),

908+

...(effectivePayload.audioAsVoice === true ? { audioAsVoice: true } : {}),

909+

...(params.forceDocument !== undefined ? { forceDocument: params.forceDocument } : {}),

960910

};

961-

const applySendReplyToConsumption = <T extends typeof sendOverrides>(overrides: T): T =>

911+

const applySendReplyToConsumption = <T extends OutboundMessageSendOverrides>(

912+

overrides: T,

913+

): T =>

962914

applyReplyToConsumption(overrides, {

963915

consumeImplicitReply: replyToResolution.source === "implicit",

964916

});

@@ -1074,32 +1026,24 @@ async function deliverOutboundPayloadsCore(

10741026

let firstMessageId: string | undefined;

10751027

let lastMessageId: string | undefined;

10761028

const beforeCount = results.length;

1077-

await sendMediaWithLeadingCaption({

1029+

const mediaUnits = planOutboundMediaMessageUnits({

10781030

mediaUrls: payloadSummary.mediaUrls,

10791031

caption: payloadSummary.text,

1080-

send: async ({ mediaUrl, caption }) => {

1081-

throwIfAborted(abortSignal);

1082-

if (handler.sendFormattedMedia) {

1083-

const delivery = await handler.sendFormattedMedia(

1084-

caption ?? "",

1085-

mediaUrl,

1086-

applySendReplyToConsumption(sendOverrides),

1087-

);

1088-

results.push(delivery);

1089-

firstMessageId ??= delivery.messageId;

1090-

lastMessageId = delivery.messageId;

1091-

return;

1092-

}

1093-

const delivery = await handler.sendMedia(

1094-

caption ?? "",

1095-

mediaUrl,

1096-

applySendReplyToConsumption(sendOverrides),

1097-

);

1098-

results.push(delivery);

1099-

firstMessageId ??= delivery.messageId;

1100-

lastMessageId = delivery.messageId;

1101-

},

1032+

overrides: sendOverrides,

1033+

consumeReplyTo: applySendReplyToConsumption,

11021034

});

1035+

for (const unit of mediaUnits) {

1036+

if (unit.kind !== "media") {

1037+

continue;

1038+

}

1039+

throwIfAborted(abortSignal);

1040+

const delivery = handler.sendFormattedMedia

1041+

? await handler.sendFormattedMedia(unit.caption ?? "", unit.mediaUrl, unit.overrides)

1042+

: await handler.sendMedia(unit.caption ?? "", unit.mediaUrl, unit.overrides);

1043+

results.push(delivery);

1044+

firstMessageId ??= delivery.messageId;

1045+

lastMessageId = delivery.messageId;

1046+

}

11031047

await maybePinDeliveredMessage({

11041048

handler,

11051049

payload: effectivePayload,