mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 08:52:12 +00:00
refactor: deduplicate reply payload helpers
This commit is contained in:
@@ -42,6 +42,7 @@ export * from "../channels/plugins/outbound/interactive.js";
|
||||
export * from "../channels/plugins/pairing-adapters.js";
|
||||
export * from "../channels/plugins/runtime-forwarders.js";
|
||||
export * from "../channels/plugins/target-resolvers.js";
|
||||
export * from "../channels/plugins/threading-helpers.js";
|
||||
export * from "../channels/plugins/status-issues/shared.js";
|
||||
export * from "../channels/plugins/whatsapp-heartbeat.js";
|
||||
export * from "../infra/outbound/send-deps.js";
|
||||
@@ -49,6 +50,7 @@ export * from "../polls.js";
|
||||
export * from "../utils/message-channel.js";
|
||||
export * from "../whatsapp/normalize.js";
|
||||
export { createActionGate, jsonResult, readStringParam } from "../agents/tools/common.js";
|
||||
export * from "./channel-send-result.js";
|
||||
export * from "./channel-lifecycle.js";
|
||||
export * from "./directory-runtime.js";
|
||||
export type {
|
||||
|
||||
120
src/plugin-sdk/channel-send-result.test.ts
Normal file
120
src/plugin-sdk/channel-send-result.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
attachChannelToResult,
|
||||
attachChannelToResults,
|
||||
buildChannelSendResult,
|
||||
createAttachedChannelResultAdapter,
|
||||
createEmptyChannelResult,
|
||||
createRawChannelSendResultAdapter,
|
||||
} from "./channel-send-result.js";
|
||||
|
||||
describe("attachChannelToResult", () => {
|
||||
it("preserves the existing result shape and stamps the channel", () => {
|
||||
expect(
|
||||
attachChannelToResult("discord", {
|
||||
messageId: "m1",
|
||||
ok: true,
|
||||
extra: "value",
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "discord",
|
||||
messageId: "m1",
|
||||
ok: true,
|
||||
extra: "value",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("attachChannelToResults", () => {
|
||||
it("stamps each result in a list with the shared channel id", () => {
|
||||
expect(
|
||||
attachChannelToResults("signal", [
|
||||
{ messageId: "m1", timestamp: 1 },
|
||||
{ messageId: "m2", timestamp: 2 },
|
||||
]),
|
||||
).toEqual([
|
||||
{ channel: "signal", messageId: "m1", timestamp: 1 },
|
||||
{ channel: "signal", messageId: "m2", timestamp: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildChannelSendResult", () => {
|
||||
it("normalizes raw send results", () => {
|
||||
const result = buildChannelSendResult("zalo", {
|
||||
ok: false,
|
||||
messageId: null,
|
||||
error: "boom",
|
||||
});
|
||||
|
||||
expect(result.channel).toBe("zalo");
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.messageId).toBe("");
|
||||
expect(result.error).toEqual(new Error("boom"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("createEmptyChannelResult", () => {
|
||||
it("builds an empty outbound result with channel metadata", () => {
|
||||
expect(createEmptyChannelResult("line", { chatId: "u1" })).toEqual({
|
||||
channel: "line",
|
||||
messageId: "",
|
||||
chatId: "u1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAttachedChannelResultAdapter", () => {
|
||||
it("wraps outbound delivery and poll results", async () => {
|
||||
const adapter = createAttachedChannelResultAdapter({
|
||||
channel: "discord",
|
||||
sendText: async () => ({ messageId: "m1", channelId: "c1" }),
|
||||
sendMedia: async () => ({ messageId: "m2" }),
|
||||
sendPoll: async () => ({ messageId: "m3", pollId: "p1" }),
|
||||
});
|
||||
|
||||
await expect(adapter.sendText!({ cfg: {} as never, to: "x", text: "hi" })).resolves.toEqual({
|
||||
channel: "discord",
|
||||
messageId: "m1",
|
||||
channelId: "c1",
|
||||
});
|
||||
await expect(adapter.sendMedia!({ cfg: {} as never, to: "x", text: "hi" })).resolves.toEqual({
|
||||
channel: "discord",
|
||||
messageId: "m2",
|
||||
});
|
||||
await expect(
|
||||
adapter.sendPoll!({
|
||||
cfg: {} as never,
|
||||
to: "x",
|
||||
poll: { question: "t", options: ["a", "b"] },
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
channel: "discord",
|
||||
messageId: "m3",
|
||||
pollId: "p1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createRawChannelSendResultAdapter", () => {
|
||||
it("normalizes raw send results", async () => {
|
||||
const adapter = createRawChannelSendResultAdapter({
|
||||
channel: "zalo",
|
||||
sendText: async () => ({ ok: true, messageId: "m1" }),
|
||||
sendMedia: async () => ({ ok: false, error: "boom" }),
|
||||
});
|
||||
|
||||
await expect(adapter.sendText!({ cfg: {} as never, to: "x", text: "hi" })).resolves.toEqual({
|
||||
channel: "zalo",
|
||||
ok: true,
|
||||
messageId: "m1",
|
||||
error: undefined,
|
||||
});
|
||||
await expect(adapter.sendMedia!({ cfg: {} as never, to: "x", text: "hi" })).resolves.toEqual({
|
||||
channel: "zalo",
|
||||
ok: false,
|
||||
messageId: "",
|
||||
error: new Error("boom"),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,74 @@
|
||||
import type { ChannelOutboundAdapter, ChannelPollResult } from "../channels/plugins/types.js";
|
||||
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
|
||||
|
||||
export type ChannelSendRawResult = {
|
||||
ok: boolean;
|
||||
messageId?: string | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export function attachChannelToResult<T extends object>(channel: string, result: T) {
|
||||
return {
|
||||
channel,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
export function attachChannelToResults<T extends object>(channel: string, results: readonly T[]) {
|
||||
return results.map((result) => attachChannelToResult(channel, result));
|
||||
}
|
||||
|
||||
export function createEmptyChannelResult(
|
||||
channel: string,
|
||||
result: Partial<Omit<OutboundDeliveryResult, "channel" | "messageId">> & {
|
||||
messageId?: string;
|
||||
} = {},
|
||||
): OutboundDeliveryResult {
|
||||
return attachChannelToResult(channel, {
|
||||
messageId: "",
|
||||
...result,
|
||||
});
|
||||
}
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
type SendTextParams = Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0];
|
||||
type SendMediaParams = Parameters<NonNullable<ChannelOutboundAdapter["sendMedia"]>>[0];
|
||||
type SendPollParams = Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0];
|
||||
|
||||
export function createAttachedChannelResultAdapter(params: {
|
||||
channel: string;
|
||||
sendText?: (ctx: SendTextParams) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
||||
sendMedia?: (ctx: SendMediaParams) => MaybePromise<Omit<OutboundDeliveryResult, "channel">>;
|
||||
sendPoll?: (ctx: SendPollParams) => MaybePromise<Omit<ChannelPollResult, "channel">>;
|
||||
}): Pick<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll"> {
|
||||
return {
|
||||
sendText: params.sendText
|
||||
? async (ctx) => attachChannelToResult(params.channel, await params.sendText!(ctx))
|
||||
: undefined,
|
||||
sendMedia: params.sendMedia
|
||||
? async (ctx) => attachChannelToResult(params.channel, await params.sendMedia!(ctx))
|
||||
: undefined,
|
||||
sendPoll: params.sendPoll
|
||||
? async (ctx) => attachChannelToResult(params.channel, await params.sendPoll!(ctx))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function createRawChannelSendResultAdapter(params: {
|
||||
channel: string;
|
||||
sendText?: (ctx: SendTextParams) => MaybePromise<ChannelSendRawResult>;
|
||||
sendMedia?: (ctx: SendMediaParams) => MaybePromise<ChannelSendRawResult>;
|
||||
}): Pick<ChannelOutboundAdapter, "sendText" | "sendMedia"> {
|
||||
return {
|
||||
sendText: params.sendText
|
||||
? async (ctx) => buildChannelSendResult(params.channel, await params.sendText!(ctx))
|
||||
: undefined,
|
||||
sendMedia: params.sendMedia
|
||||
? async (ctx) => buildChannelSendResult(params.channel, await params.sendMedia!(ctx))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize raw channel send results into the shape shared outbound callers expect. */
|
||||
export function buildChannelSendResult(channel: string, result: ChannelSendRawResult) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DiscordSendResult } from "../../extensions/discord/api.js";
|
||||
import { attachChannelToResult } from "./channel-send-result.js";
|
||||
|
||||
type DiscordSendOptionInput = {
|
||||
replyToId?: string | null;
|
||||
@@ -32,5 +33,5 @@ export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput)
|
||||
|
||||
/** Stamp raw Discord send results with the channel id expected by shared outbound flows. */
|
||||
export function tagDiscordChannelResult(result: DiscordSendResult) {
|
||||
return { channel: "discord" as const, ...result };
|
||||
return attachChannelToResult("discord", result);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export { ircSetupAdapter, ircSetupWizard } from "../../extensions/irc/api.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
createNormalizedOutboundDeliverer,
|
||||
deliverFormattedTextWithAttachments,
|
||||
formatTextWithAttachmentLinks,
|
||||
resolveOutboundMediaUrls,
|
||||
} from "./reply-payload.js";
|
||||
|
||||
@@ -46,6 +46,7 @@ export {
|
||||
splitSetupEntries,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
|
||||
export { resolveOutboundMediaUrls } from "./reply-payload.js";
|
||||
export type {
|
||||
BaseProbeResult,
|
||||
ChannelDirectoryEntry,
|
||||
|
||||
@@ -94,6 +94,7 @@ export { createPersistentDedupe } from "./persistent-dedupe.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
createNormalizedOutboundDeliverer,
|
||||
deliverFormattedTextWithAttachments,
|
||||
formatTextWithAttachmentLinks,
|
||||
resolveOutboundMediaUrls,
|
||||
} from "./reply-payload.js";
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isNumericTargetId, sendPayloadWithChunkedTextAndMedia } from "./reply-payload.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
deliverFormattedTextWithAttachments,
|
||||
deliverTextOrMediaReply,
|
||||
isNumericTargetId,
|
||||
resolveOutboundMediaUrls,
|
||||
resolveTextChunksWithFallback,
|
||||
sendMediaWithLeadingCaption,
|
||||
sendPayloadWithChunkedTextAndMedia,
|
||||
} from "./reply-payload.js";
|
||||
|
||||
describe("sendPayloadWithChunkedTextAndMedia", () => {
|
||||
it("returns empty result when payload has no text and no media", async () => {
|
||||
@@ -56,3 +64,155 @@ describe("sendPayloadWithChunkedTextAndMedia", () => {
|
||||
expect(isNumericTargetId("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveOutboundMediaUrls", () => {
|
||||
it("prefers mediaUrls over the legacy single-media field", () => {
|
||||
expect(
|
||||
resolveOutboundMediaUrls({
|
||||
mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"],
|
||||
mediaUrl: "https://example.com/legacy.png",
|
||||
}),
|
||||
).toEqual(["https://example.com/a.png", "https://example.com/b.png"]);
|
||||
});
|
||||
|
||||
it("falls back to the legacy single-media field", () => {
|
||||
expect(
|
||||
resolveOutboundMediaUrls({
|
||||
mediaUrl: "https://example.com/legacy.png",
|
||||
}),
|
||||
).toEqual(["https://example.com/legacy.png"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveTextChunksWithFallback", () => {
|
||||
it("returns existing chunks unchanged", () => {
|
||||
expect(resolveTextChunksWithFallback("hello", ["a", "b"])).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("falls back to the full text when chunkers return nothing", () => {
|
||||
expect(resolveTextChunksWithFallback("hello", [])).toEqual(["hello"]);
|
||||
});
|
||||
|
||||
it("returns empty for empty text with no chunks", () => {
|
||||
expect(resolveTextChunksWithFallback("", [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deliverTextOrMediaReply", () => {
|
||||
it("sends media first with caption only on the first attachment", async () => {
|
||||
const sendMedia = vi.fn(async () => undefined);
|
||||
const sendText = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
deliverTextOrMediaReply({
|
||||
payload: { text: "hello", mediaUrls: ["https://a", "https://b"] },
|
||||
text: "hello",
|
||||
sendText,
|
||||
sendMedia,
|
||||
}),
|
||||
).resolves.toBe("media");
|
||||
|
||||
expect(sendMedia).toHaveBeenNthCalledWith(1, {
|
||||
mediaUrl: "https://a",
|
||||
caption: "hello",
|
||||
});
|
||||
expect(sendMedia).toHaveBeenNthCalledWith(2, {
|
||||
mediaUrl: "https://b",
|
||||
caption: undefined,
|
||||
});
|
||||
expect(sendText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to chunked text delivery when there is no media", async () => {
|
||||
const sendMedia = vi.fn(async () => undefined);
|
||||
const sendText = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
deliverTextOrMediaReply({
|
||||
payload: { text: "alpha beta gamma" },
|
||||
text: "alpha beta gamma",
|
||||
chunkText: () => ["alpha", "beta", "gamma"],
|
||||
sendText,
|
||||
sendMedia,
|
||||
}),
|
||||
).resolves.toBe("text");
|
||||
|
||||
expect(sendText).toHaveBeenCalledTimes(3);
|
||||
expect(sendText).toHaveBeenNthCalledWith(1, "alpha");
|
||||
expect(sendText).toHaveBeenNthCalledWith(2, "beta");
|
||||
expect(sendText).toHaveBeenNthCalledWith(3, "gamma");
|
||||
expect(sendMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns empty when chunking produces no sendable text", async () => {
|
||||
const sendMedia = vi.fn(async () => undefined);
|
||||
const sendText = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
deliverTextOrMediaReply({
|
||||
payload: { text: " " },
|
||||
text: " ",
|
||||
chunkText: () => [],
|
||||
sendText,
|
||||
sendMedia,
|
||||
}),
|
||||
).resolves.toBe("empty");
|
||||
|
||||
expect(sendText).not.toHaveBeenCalled();
|
||||
expect(sendMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMediaWithLeadingCaption", () => {
|
||||
it("passes leading-caption metadata to async error handlers", async () => {
|
||||
const send = vi
|
||||
.fn<({ mediaUrl, caption }: { mediaUrl: string; caption?: string }) => Promise<void>>()
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
const onError = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
sendMediaWithLeadingCaption({
|
||||
mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"],
|
||||
caption: "hello",
|
||||
send,
|
||||
onError,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://example.com/a.png",
|
||||
caption: "hello",
|
||||
index: 0,
|
||||
isFirst: true,
|
||||
}),
|
||||
);
|
||||
expect(send).toHaveBeenNthCalledWith(2, {
|
||||
mediaUrl: "https://example.com/b.png",
|
||||
caption: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deliverFormattedTextWithAttachments", () => {
|
||||
it("combines attachment links and forwards replyToId", async () => {
|
||||
const send = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
deliverFormattedTextWithAttachments({
|
||||
payload: {
|
||||
text: "hello",
|
||||
mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"],
|
||||
replyToId: "r1",
|
||||
},
|
||||
send,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(send).toHaveBeenCalledWith({
|
||||
text: "hello\n\nAttachment: https://example.com/a.png\nAttachment: https://example.com/b.png",
|
||||
replyToId: "r1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,17 @@ export function resolveOutboundMediaUrls(payload: {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Preserve caller-provided chunking, but fall back to the full text when chunkers return nothing. */
|
||||
export function resolveTextChunksWithFallback(text: string, chunks: readonly string[]): string[] {
|
||||
if (chunks.length > 0) {
|
||||
return [...chunks];
|
||||
}
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
return [text];
|
||||
}
|
||||
|
||||
/** Send media-first payloads intact, or chunk text-only payloads through the caller's transport hooks. */
|
||||
export async function sendPayloadWithChunkedTextAndMedia<
|
||||
TContext extends { payload: object },
|
||||
@@ -129,21 +140,32 @@ export async function sendMediaWithLeadingCaption(params: {
|
||||
mediaUrls: string[];
|
||||
caption: string;
|
||||
send: (payload: { mediaUrl: string; caption?: string }) => Promise<void>;
|
||||
onError?: (error: unknown, mediaUrl: string) => void;
|
||||
onError?: (params: {
|
||||
error: unknown;
|
||||
mediaUrl: string;
|
||||
caption?: string;
|
||||
index: number;
|
||||
isFirst: boolean;
|
||||
}) => Promise<void> | void;
|
||||
}): Promise<boolean> {
|
||||
if (params.mediaUrls.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let first = true;
|
||||
for (const mediaUrl of params.mediaUrls) {
|
||||
const caption = first ? params.caption : undefined;
|
||||
first = false;
|
||||
for (const [index, mediaUrl] of params.mediaUrls.entries()) {
|
||||
const isFirst = index === 0;
|
||||
const caption = isFirst ? params.caption : undefined;
|
||||
try {
|
||||
await params.send({ mediaUrl, caption });
|
||||
} catch (error) {
|
||||
if (params.onError) {
|
||||
params.onError(error, mediaUrl);
|
||||
await params.onError({
|
||||
error,
|
||||
mediaUrl,
|
||||
caption,
|
||||
index,
|
||||
isFirst,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
@@ -151,3 +173,60 @@ export async function sendMediaWithLeadingCaption(params: {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deliverTextOrMediaReply(params: {
|
||||
payload: OutboundReplyPayload;
|
||||
text: string;
|
||||
chunkText?: (text: string) => readonly string[];
|
||||
sendText: (text: string) => Promise<void>;
|
||||
sendMedia: (payload: { mediaUrl: string; caption?: string }) => Promise<void>;
|
||||
onMediaError?: (params: {
|
||||
error: unknown;
|
||||
mediaUrl: string;
|
||||
caption?: string;
|
||||
index: number;
|
||||
isFirst: boolean;
|
||||
}) => Promise<void> | void;
|
||||
}): Promise<"empty" | "text" | "media"> {
|
||||
const mediaUrls = resolveOutboundMediaUrls(params.payload);
|
||||
const sentMedia = await sendMediaWithLeadingCaption({
|
||||
mediaUrls,
|
||||
caption: params.text,
|
||||
send: params.sendMedia,
|
||||
onError: params.onMediaError,
|
||||
});
|
||||
if (sentMedia) {
|
||||
return "media";
|
||||
}
|
||||
if (!params.text) {
|
||||
return "empty";
|
||||
}
|
||||
const chunks = params.chunkText ? params.chunkText(params.text) : [params.text];
|
||||
let sentText = false;
|
||||
for (const chunk of chunks) {
|
||||
if (!chunk) {
|
||||
continue;
|
||||
}
|
||||
await params.sendText(chunk);
|
||||
sentText = true;
|
||||
}
|
||||
return sentText ? "text" : "empty";
|
||||
}
|
||||
|
||||
export async function deliverFormattedTextWithAttachments(params: {
|
||||
payload: OutboundReplyPayload;
|
||||
send: (params: { text: string; replyToId?: string }) => Promise<void>;
|
||||
}): Promise<boolean> {
|
||||
const text = formatTextWithAttachmentLinks(
|
||||
params.payload.text,
|
||||
resolveOutboundMediaUrls(params.payload),
|
||||
);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
await params.send({
|
||||
text,
|
||||
replyToId: params.payload.replyToId,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as channelRuntimeSdk from "openclaw/plugin-sdk/channel-runtime";
|
||||
import * as channelSendResultSdk from "openclaw/plugin-sdk/channel-send-result";
|
||||
import * as compatSdk from "openclaw/plugin-sdk/compat";
|
||||
import * as coreSdk from "openclaw/plugin-sdk/core";
|
||||
import type {
|
||||
@@ -16,6 +17,7 @@ import * as msteamsSdk from "openclaw/plugin-sdk/msteams";
|
||||
import * as nostrSdk from "openclaw/plugin-sdk/nostr";
|
||||
import * as ollamaSetupSdk from "openclaw/plugin-sdk/ollama-setup";
|
||||
import * as providerSetupSdk from "openclaw/plugin-sdk/provider-setup";
|
||||
import * as replyPayloadSdk from "openclaw/plugin-sdk/reply-payload";
|
||||
import * as routingSdk from "openclaw/plugin-sdk/routing";
|
||||
import * as runtimeSdk from "openclaw/plugin-sdk/runtime";
|
||||
import * as sandboxSdk from "openclaw/plugin-sdk/sandbox";
|
||||
@@ -93,6 +95,16 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(typeof routingSdk.resolveThreadSessionKeys).toBe("function");
|
||||
});
|
||||
|
||||
it("exports reply payload helpers from the dedicated subpath", () => {
|
||||
expect(typeof replyPayloadSdk.deliverFormattedTextWithAttachments).toBe("function");
|
||||
expect(typeof replyPayloadSdk.deliverTextOrMediaReply).toBe("function");
|
||||
expect(typeof replyPayloadSdk.formatTextWithAttachmentLinks).toBe("function");
|
||||
expect(typeof replyPayloadSdk.resolveOutboundMediaUrls).toBe("function");
|
||||
expect(typeof replyPayloadSdk.resolveTextChunksWithFallback).toBe("function");
|
||||
expect(typeof replyPayloadSdk.sendMediaWithLeadingCaption).toBe("function");
|
||||
expect(typeof replyPayloadSdk.sendPayloadWithChunkedTextAndMedia).toBe("function");
|
||||
});
|
||||
|
||||
it("exports account helper builders from the dedicated subpath", () => {
|
||||
expect(typeof accountHelpersSdk.createAccountListHelpers).toBe("function");
|
||||
});
|
||||
@@ -122,17 +134,36 @@ describe("plugin-sdk subpath exports", () => {
|
||||
});
|
||||
|
||||
it("exports channel runtime helpers from the dedicated subpath", () => {
|
||||
expect(typeof channelRuntimeSdk.attachChannelToResult).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.attachChannelToResults).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.buildUnresolvedTargetResults).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createAttachedChannelResultAdapter).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createChannelDirectoryAdapter).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createEmptyChannelResult).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createEmptyChannelDirectoryAdapter).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createRawChannelSendResultAdapter).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createLoggedPairingApprovalNotifier).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createPairingPrefixStripper).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createScopedAccountReplyToModeResolver).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createStaticReplyToModeResolver).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createTopLevelChannelReplyToModeResolver).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createRuntimeDirectoryLiveAdapter).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createRuntimeOutboundDelegates).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.sendPayloadMediaSequenceAndFinalize).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.sendPayloadMediaSequenceOrFallback).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.resolveTargetsWithOptionalToken).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createTextPairingAdapter).toBe("function");
|
||||
});
|
||||
|
||||
it("exports channel send-result helpers from the dedicated subpath", () => {
|
||||
expect(typeof channelSendResultSdk.attachChannelToResult).toBe("function");
|
||||
expect(typeof channelSendResultSdk.attachChannelToResults).toBe("function");
|
||||
expect(typeof channelSendResultSdk.buildChannelSendResult).toBe("function");
|
||||
expect(typeof channelSendResultSdk.createAttachedChannelResultAdapter).toBe("function");
|
||||
expect(typeof channelSendResultSdk.createEmptyChannelResult).toBe("function");
|
||||
expect(typeof channelSendResultSdk.createRawChannelSendResultAdapter).toBe("function");
|
||||
});
|
||||
|
||||
it("exports provider setup helpers from the dedicated subpath", () => {
|
||||
expect(typeof providerSetupSdk.buildVllmProvider).toBe("function");
|
||||
expect(typeof providerSetupSdk.discoverOpenAICompatibleSelfHostedProvider).toBe("function");
|
||||
|
||||
@@ -77,6 +77,7 @@ export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { buildChannelSendResult } from "./channel-send-result.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
deliverTextOrMediaReply,
|
||||
isNumericTargetId,
|
||||
resolveOutboundMediaUrls,
|
||||
sendMediaWithLeadingCaption,
|
||||
|
||||
@@ -68,6 +68,7 @@ export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { buildChannelSendResult } from "./channel-send-result.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
deliverTextOrMediaReply,
|
||||
isNumericTargetId,
|
||||
resolveOutboundMediaUrls,
|
||||
sendMediaWithLeadingCaption,
|
||||
|
||||
Reference in New Issue
Block a user