mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
refactor(outbound): share reply fanout policy
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
createReplyToFanout,
|
||||
resolveOutboundSendDep,
|
||||
type ReplyToResolution,
|
||||
type OutboundSendDeps,
|
||||
} from "openclaw/plugin-sdk/outbound-runtime";
|
||||
import { isSingleUseReplyToMode } from "openclaw/plugin-sdk/reply-reference";
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeOptionalStringifiedId,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
import { normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { withDiscordDeliveryRetry } from "./delivery-retry.js";
|
||||
|
||||
type DiscordSendRuntime = typeof import("./send.js");
|
||||
@@ -54,31 +52,13 @@ export function resolveDiscordFormattingOptions(ctx: {
|
||||
};
|
||||
}
|
||||
|
||||
export function createResolvedReplyToFanout(params: {
|
||||
replyToId?: string | null;
|
||||
replyToMode?: ReplyToMode;
|
||||
}): () => string | undefined {
|
||||
const replyToId = normalizeOptionalString(params.replyToId);
|
||||
if (!replyToId) {
|
||||
return () => undefined;
|
||||
}
|
||||
if (!params.replyToMode || !isSingleUseReplyToMode(params.replyToMode)) {
|
||||
return () => replyToId;
|
||||
}
|
||||
let current: string | undefined = replyToId;
|
||||
return () => {
|
||||
const value = current;
|
||||
current = undefined;
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
export async function createDiscordPayloadSendContext(ctx: {
|
||||
cfg: OpenClawConfig;
|
||||
to: string;
|
||||
accountId?: string | null;
|
||||
deps?: OutboundSendDeps;
|
||||
replyToId?: string | null;
|
||||
replyToIdSource?: ReplyToResolution["source"];
|
||||
replyToMode?: ReplyToMode;
|
||||
formatting?: DiscordFormattingOptions;
|
||||
threadId?: string | number | null;
|
||||
@@ -94,8 +74,9 @@ export async function createDiscordPayloadSendContext(ctx: {
|
||||
return {
|
||||
target: resolveDiscordOutboundTarget({ to: ctx.to, threadId: ctx.threadId }),
|
||||
formatting: resolveDiscordFormattingOptions(ctx),
|
||||
resolveReplyTo: createResolvedReplyToFanout({
|
||||
resolveReplyTo: createReplyToFanout({
|
||||
replyToId: ctx.replyToId,
|
||||
replyToIdSource: ctx.replyToIdSource,
|
||||
replyToMode: ctx.replyToMode,
|
||||
}),
|
||||
send: resolveOutboundSendDep<DiscordSendFn>(ctx.deps, "discord") ?? runtime.sendMessageDiscord,
|
||||
|
||||
34
src/infra/outbound/reply-policy.test.ts
Normal file
34
src/infra/outbound/reply-policy.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createReplyToFanout } from "./reply-policy.js";
|
||||
|
||||
describe("createReplyToFanout", () => {
|
||||
it("consumes implicit single-use replies once", () => {
|
||||
const next = createReplyToFanout({
|
||||
replyToId: "reply-1",
|
||||
replyToIdSource: "implicit",
|
||||
replyToMode: "first",
|
||||
});
|
||||
|
||||
expect([next(), next(), next()]).toEqual(["reply-1", undefined, undefined]);
|
||||
});
|
||||
|
||||
it("keeps explicit replies reusable even in single-use modes", () => {
|
||||
const next = createReplyToFanout({
|
||||
replyToId: "reply-1",
|
||||
replyToIdSource: "explicit",
|
||||
replyToMode: "first",
|
||||
});
|
||||
|
||||
expect([next(), next()]).toEqual(["reply-1", "reply-1"]);
|
||||
});
|
||||
|
||||
it("keeps all-mode replies reusable", () => {
|
||||
const next = createReplyToFanout({
|
||||
replyToId: "reply-1",
|
||||
replyToIdSource: "implicit",
|
||||
replyToMode: "all",
|
||||
});
|
||||
|
||||
expect([next(), next()]).toEqual(["reply-1", "reply-1"]);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,30 @@ export type ReplyToResolution = {
|
||||
source?: "explicit" | "implicit";
|
||||
};
|
||||
|
||||
export function createReplyToFanout(params: {
|
||||
replyToId?: string | null;
|
||||
replyToMode?: ReplyToMode;
|
||||
replyToIdSource?: ReplyToResolution["source"];
|
||||
}): () => string | undefined {
|
||||
const replyToId = params.replyToId ?? undefined;
|
||||
if (!replyToId) {
|
||||
return () => undefined;
|
||||
}
|
||||
const singleUse =
|
||||
params.replyToIdSource !== "explicit" &&
|
||||
params.replyToMode !== undefined &&
|
||||
isSingleUseReplyToMode(params.replyToMode);
|
||||
if (!singleUse) {
|
||||
return () => replyToId;
|
||||
}
|
||||
let current: string | undefined = replyToId;
|
||||
return () => {
|
||||
const value = current;
|
||||
current = undefined;
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
export function createReplyToDeliveryPolicy(params: {
|
||||
replyToId?: string | null;
|
||||
replyToMode?: ReplyToMode;
|
||||
|
||||
@@ -2,6 +2,7 @@ export { createRuntimeOutboundDelegates } from "../channels/plugins/runtime-forw
|
||||
export { resolveOutboundSendDep, type OutboundSendDeps } from "../infra/outbound/send-deps.js";
|
||||
export { resolveAgentOutboundIdentity, type OutboundIdentity } from "../infra/outbound/identity.js";
|
||||
export type { OutboundDeliveryFormattingOptions } from "../infra/outbound/formatting.js";
|
||||
export { createReplyToFanout, type ReplyToResolution } from "../infra/outbound/reply-policy.js";
|
||||
export {
|
||||
deliverOutboundPayloads,
|
||||
type DeliverOutboundPayloadsParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReplyPayload as InternalReplyPayload } from "../auto-reply/reply-payload.js";
|
||||
import { isSingleUseReplyToMode } from "../auto-reply/reply/reply-reference.js";
|
||||
import type { ChannelOutboundAdapter } from "../channels/plugins/outbound.types.js";
|
||||
import { createReplyToFanout } from "../infra/outbound/reply-policy.js";
|
||||
import { normalizeLowercaseStringOrEmpty, readStringValue } from "../shared/string-coerce.js";
|
||||
|
||||
export type { MediaPayload, MediaPayloadInput } from "../channels/plugins/media-payload.js";
|
||||
@@ -39,26 +39,6 @@ type SendPayloadAdapter = Pick<
|
||||
|
||||
const REASONING_PREFIX = "reasoning:";
|
||||
|
||||
function createSendPayloadReplyToFanout(ctx: SendPayloadContext): () => string | undefined {
|
||||
const replyToId = ctx.replyToId ?? undefined;
|
||||
if (!replyToId) {
|
||||
return () => undefined;
|
||||
}
|
||||
const singleUse =
|
||||
ctx.replyToIdSource !== "explicit" &&
|
||||
ctx.replyToMode !== undefined &&
|
||||
isSingleUseReplyToMode(ctx.replyToMode);
|
||||
if (!singleUse) {
|
||||
return () => replyToId;
|
||||
}
|
||||
let current: string | undefined = replyToId;
|
||||
return () => {
|
||||
const value = current;
|
||||
current = undefined;
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
function trimLeadingMarkdownQuoteMarkers(text: string): string {
|
||||
let candidate = text.trimStart();
|
||||
while (candidate.startsWith(">")) {
|
||||
@@ -310,7 +290,7 @@ export async function sendTextMediaPayload(params: {
|
||||
if (!text && urls.length === 0) {
|
||||
return { channel: params.channel, messageId: "" };
|
||||
}
|
||||
const nextReplyToId = createSendPayloadReplyToFanout(params.ctx);
|
||||
const nextReplyToId = createReplyToFanout(params.ctx);
|
||||
if (urls.length > 0) {
|
||||
const lastResult = await sendPayloadMediaSequence({
|
||||
text,
|
||||
|
||||
Reference in New Issue
Block a user